From 6b7684759559e6d3fbe51575305a3849ee3dbd83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 23:25:33 +0000 Subject: [PATCH 01/74] Chore(deps-dev): Bump the development group with 2 updates (#7723) * Chore(deps-dev): Bump the development group with 2 updates Bumps the development group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [pytest](https://github.com/pytest-dev/pytest). Updates `ruff` from 0.6.4 to 0.6.5 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.4...0.6.5) Updates `pytest` from 8.3.2 to 8.3.3 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch dependency-group: development - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch dependency-group: development ... Signed-off-by: dependabot[bot] * Update .pre-commit-config.yaml --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- Pipfile.lock | 44 ++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 345677c05..c39480421 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: exclude: "(^Pipfile\\.lock$)" # Python hooks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.6.4' + rev: 'v0.6.5' hooks: - id: ruff - id: ruff-format diff --git a/Pipfile.lock b/Pipfile.lock index 48a9ad907..4b94f6dfe 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -3473,12 +3473,12 @@ }, "pytest": { "hashes": [ - "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", - "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce" + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.3.2" + "version": "==8.3.3" }, "pytest-cov": { "hashes": [ @@ -3760,28 +3760,28 @@ }, "ruff": { "hashes": [ - "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6", - "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa", - "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6", - "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1", - "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e", - "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58", - "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa", - "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc", - "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d", - "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408", - "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212", - "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14", - "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60", - "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818", - "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258", - "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f", - "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617", - "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523" + "sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae", + "sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810", + "sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200", + "sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680", + "sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9", + "sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc", + "sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb", + "sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0", + "sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276", + "sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19", + "sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972", + "sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748", + "sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178", + "sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253", + "sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69", + "sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c", + "sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f", + "sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.6.4" + "version": "==0.6.5" }, "scipy": { "hashes": [ From 7f04c6a09ae15c0f9f8bd5c35b30183bde7b25fe Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 16 Sep 2024 19:36:44 -0700 Subject: [PATCH 02/74] Fix a random test error --- .../filterable-dropdown.component.spec.ts | 7 +------ .../filterable-dropdown/filterable-dropdown.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts index a285144f4..d1c37bc8b 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts @@ -539,15 +539,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => fixture.nativeElement .querySelector('button') .dispatchEvent(new MouseEvent('click')) // open - fixture.detectChanges() tick(100) component.filterText = 'FooBar' - fixture.detectChanges() - component.listFilterTextInput.nativeElement.dispatchEvent( - new KeyboardEvent('keyup', { key: 'Enter' }) - ) + component.listFilterEnter() expect(component.selectionModel.getSelectedItems()).toEqual([]) - tick(300) expect(createSpy).toHaveBeenCalled() })) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index 4a3c70953..7830e3909 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -483,7 +483,7 @@ export class FilterableDropdownComponent implements OnDestroy, OnInit { dropdownOpenChange(open: boolean): void { if (open) { setTimeout(() => { - this.listFilterTextInput.nativeElement.focus() + this.listFilterTextInput?.nativeElement.focus() }, 0) if (this.editing) { this.selectionModel.reset() @@ -492,7 +492,7 @@ export class FilterableDropdownComponent implements OnDestroy, OnInit { this.opened.next(this) } else { if (this.creating) { - this.dropdown.open() + this.dropdown?.open() this.creating = false } else { this.filterText = '' From 4549220a9bd6356444be047fb61659a3b08e34d6 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:02:51 -0700 Subject: [PATCH 03/74] Feature: copy workflows and mail rules, improve layout (#7727) --- src-ui/messages.xlf | 118 ++++++++++++------ .../manage/mail/mail.component.html | 62 ++++++--- .../manage/mail/mail.component.scss | 4 + .../manage/mail/mail.component.spec.ts | 11 ++ .../components/manage/mail/mail.component.ts | 14 ++- .../manage/workflows/workflows.component.html | 42 +++++-- .../manage/workflows/workflows.component.scss | 4 + .../workflows/workflows.component.spec.ts | 16 ++- .../manage/workflows/workflows.component.ts | 26 +++- 9 files changed, 227 insertions(+), 70 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 18ba85ce4..dec952685 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -726,7 +726,7 @@ src/app/components/manage/mail/mail.component.html - 105 + 137 src/app/components/manage/management-list/management-list.component.html @@ -1092,11 +1092,19 @@ src/app/components/manage/mail/mail.component.html - 39 + 41 src/app/components/manage/mail/mail.component.html - 85 + 51 + + + src/app/components/manage/mail/mail.component.html + 99 + + + src/app/components/manage/mail/mail.component.html + 111 src/app/components/manage/management-list/management-list.component.html @@ -1398,7 +1406,7 @@ src/app/components/manage/mail/mail.component.html - 69 + 81 src/app/components/manage/management-list/management-list.component.html @@ -1493,7 +1501,15 @@ src/app/components/manage/mail/mail.component.html - 88 + 54 + + + src/app/components/manage/mail/mail.component.html + 100 + + + src/app/components/manage/mail/mail.component.html + 114 src/app/components/manage/management-list/management-list.component.html @@ -1549,7 +1565,11 @@ src/app/components/manage/workflows/workflows.component.html - 38 + 41 + + + src/app/components/manage/workflows/workflows.component.html + 52 @@ -1879,7 +1899,7 @@ src/app/components/manage/mail/mail.component.html - 66 + 78 src/app/components/manage/management-list/management-list.component.html @@ -2207,7 +2227,7 @@ src/app/components/manage/mail/mail.component.ts - 173 + 179 src/app/components/manage/management-list/management-list.component.ts @@ -2215,7 +2235,7 @@ src/app/components/manage/workflows/workflows.component.ts - 97 + 115 @@ -2402,11 +2422,19 @@ src/app/components/manage/mail/mail.component.html - 36 + 40 src/app/components/manage/mail/mail.component.html - 82 + 48 + + + src/app/components/manage/mail/mail.component.html + 98 + + + src/app/components/manage/mail/mail.component.html + 108 src/app/components/manage/management-list/management-list.component.html @@ -2442,7 +2470,11 @@ src/app/components/manage/workflows/workflows.component.html - 35 + 40 + + + src/app/components/manage/workflows/workflows.component.html + 49 @@ -2546,7 +2578,7 @@ src/app/components/manage/mail/mail.component.ts - 175 + 181 src/app/components/manage/management-list/management-list.component.ts @@ -2554,7 +2586,7 @@ src/app/components/manage/workflows/workflows.component.ts - 99 + 117 @@ -3665,7 +3697,7 @@ src/app/components/manage/mail/mail.component.html - 68 + 80 @@ -5025,6 +5057,22 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 156 + + src/app/components/manage/mail/mail.component.html + 101 + + + src/app/components/manage/mail/mail.component.html + 119 + + + src/app/components/manage/workflows/workflows.component.html + 42 + + + src/app/components/manage/workflows/workflows.component.html + 57 + Regenerate auth token @@ -7420,35 +7468,35 @@ No mail accounts defined. src/app/components/manage/mail/mail.component.html - 50 + 62 Mail rules src/app/components/manage/mail/mail.component.html - 58 + 70 Add Rule src/app/components/manage/mail/mail.component.html - 60 + 72 Sort Order src/app/components/manage/mail/mail.component.html - 67 + 79 No mail rules defined. src/app/components/manage/mail/mail.component.html - 96 + 128 @@ -7511,56 +7559,56 @@ Saved rule "". src/app/components/manage/mail/mail.component.ts - 152 + 151 Error saving rule. src/app/components/manage/mail/mail.component.ts - 163 + 162 Confirm delete mail rule src/app/components/manage/mail/mail.component.ts - 171 + 177 This operation will permanently delete this mail rule. src/app/components/manage/mail/mail.component.ts - 172 + 178 Deleted mail rule src/app/components/manage/mail/mail.component.ts - 181 + 187 Error deleting mail rule. src/app/components/manage/mail/mail.component.ts - 190 + 196 Permissions updated src/app/components/manage/mail/mail.component.ts - 212 + 218 Error updating permissions src/app/components/manage/mail/mail.component.ts - 217 + 223 src/app/components/manage/management-list/management-list.component.ts @@ -7821,49 +7869,49 @@ No workflows defined. src/app/components/manage/workflows/workflows.component.html - 46 + 66 Saved workflow "". src/app/components/manage/workflows/workflows.component.ts - 79 + 78 Error saving workflow. src/app/components/manage/workflows/workflows.component.ts - 87 + 86 Confirm delete workflow src/app/components/manage/workflows/workflows.component.ts - 95 + 113 This operation will permanently delete this workflow. src/app/components/manage/workflows/workflows.component.ts - 96 + 114 Deleted workflow src/app/components/manage/workflows/workflows.component.ts - 105 + 123 Error deleting workflow. src/app/components/manage/workflows/workflows.component.ts - 110 + 128 diff --git a/src-ui/src/app/components/manage/mail/mail.component.html b/src-ui/src/app/components/manage/mail/mail.component.html index b804b0f2f..add5614c4 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.html +++ b/src-ui/src/app/components/manage/mail/mail.component.html @@ -19,7 +19,7 @@
Name
Server
-
Username
+
Username
Actions
@@ -29,9 +29,21 @@
{{account.imap_server}}
-
{{account.username}}
+
{{account.username}}
-
+
+
+ +
+ + + +
+
+
+
@@ -64,7 +76,7 @@
  • Name
    -
    Sort Order
    +
    Sort Order
    Account
    Actions
    @@ -74,19 +86,39 @@
  • -
    {{rule.order}}
    +
    {{rule.order}}
    {{(mailAccountService.getCached(rule.account) | async)?.name}}
    -
    - - - +
    +
    + +
    + + + + +
    +
    +
    +
    diff --git a/src-ui/src/app/components/manage/mail/mail.component.scss b/src-ui/src/app/components/manage/mail/mail.component.scss index e69de29bb..0c1f432aa 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.scss +++ b/src-ui/src/app/components/manage/mail/mail.component.scss @@ -0,0 +1,4 @@ +// hide caret on mobile dropdown +.d-block.d-sm-none .dropdown-toggle::after { + display: none; +} diff --git a/src-ui/src/app/components/manage/mail/mail.component.spec.ts b/src-ui/src/app/components/manage/mail/mail.component.spec.ts index e72b49b0f..4a134b880 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.spec.ts +++ b/src-ui/src/app/components/manage/mail/mail.component.spec.ts @@ -226,6 +226,17 @@ describe('MailComponent', () => { component.editMailRule() }) + it('should support copy mail rule', () => { + completeSetup() + let modal: NgbModalRef + modalService.activeInstances.subscribe((refs) => (modal = refs[0])) + component.copyMailRule(mailRules[0] as MailRule) + const editDialog = modal.componentInstance as MailRuleEditDialogComponent + expect(editDialog.object.id).toBeNull() + expect(editDialog.object.name).toEqual(`${mailRules[0].name} (copy)`) + expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE) + }) + it('should support delete mail rule, show error if needed', () => { completeSetup() let modal: NgbModalRef diff --git a/src-ui/src/app/components/manage/mail/mail.component.ts b/src-ui/src/app/components/manage/mail/mail.component.ts index d8820ed38..5d00b6c13 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.ts +++ b/src-ui/src/app/components/manage/mail/mail.component.ts @@ -137,14 +137,13 @@ export class MailComponent }) } - editMailRule(rule: MailRule = null) { + editMailRule(rule: MailRule = null, forceCreate = false) { const modal = this.modalService.open(MailRuleEditDialogComponent, { backdrop: 'static', size: 'xl', }) - modal.componentInstance.dialogMode = rule - ? EditDialogMode.EDIT - : EditDialogMode.CREATE + modal.componentInstance.dialogMode = + rule && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE modal.componentInstance.object = rule modal.componentInstance.succeeded .pipe(takeUntil(this.unsubscribeNotifier)) @@ -164,6 +163,13 @@ export class MailComponent }) } + copyMailRule(rule: MailRule) { + const clone = { ...rule } + clone.id = null + clone.name = `${rule.name} (copy)` + this.editMailRule(clone, true) + } + deleteMailRule(rule: MailRule) { const modal = this.modalService.open(ConfirmDialogComponent, { backdrop: 'static', diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.html b/src-ui/src/app/components/manage/workflows/workflows.component.html index d398b95b1..1e83efd36 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.html +++ b/src-ui/src/app/components/manage/workflows/workflows.component.html @@ -15,9 +15,9 @@
  • Name
    -
    Sort order
    +
    Sort order
    Status
    -
    Triggers
    +
    Triggers
    Actions
  • @@ -26,17 +26,37 @@
  • -
    {{workflow.order}}
    +
    {{workflow.order}}
    @if(workflow.enabled) { Enabled } @else { Disabled }
    -
    {{getTypesList(workflow)}}
    +
    {{getTypesList(workflow)}}
    -
    - - + +
    +
    + +
    + + + +
    +
    +
    +
    diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.scss b/src-ui/src/app/components/manage/workflows/workflows.component.scss index e69de29bb..0c1f432aa 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.scss +++ b/src-ui/src/app/components/manage/workflows/workflows.component.scss @@ -0,0 +1,4 @@ +// hide caret on mobile dropdown +.d-block.d-sm-none .dropdown-toggle::after { + display: none; +} diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts index 5a73b07b0..0bccbad2d 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts @@ -26,6 +26,7 @@ import { import { WorkflowActionType } from 'src/app/data/workflow-action' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' const workflows: Workflow[] = [ { @@ -173,6 +174,19 @@ describe('WorkflowsComponent', () => { expect(reloadSpy).toHaveBeenCalled() }) + it('should support copy', () => { + let modal: NgbModalRef + modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) + + const copyButton = fixture.debugElement.queryAll(By.css('button'))[6] + copyButton.triggerEventHandler('click') + + expect(modal).not.toBeUndefined() + const editDialog = modal.componentInstance as WorkflowEditDialogComponent + expect(editDialog.object.name).toEqual(workflows[0].name + ' (copy)') + expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE) + }) + it('should support delete, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) @@ -180,7 +194,7 @@ describe('WorkflowsComponent', () => { const deleteSpy = jest.spyOn(workflowService, 'delete') const reloadSpy = jest.spyOn(component, 'reload') - const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4] + const deleteButton = fixture.debugElement.queryAll(By.css('button'))[5] deleteButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.ts b/src-ui/src/app/components/manage/workflows/workflows.component.ts index a80f03577..92b421e9f 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.ts @@ -57,14 +57,13 @@ export class WorkflowsComponent .join(', ') } - editWorkflow(workflow: Workflow) { + editWorkflow(workflow: Workflow, forceCreate: boolean = false) { const modal = this.modalService.open(WorkflowEditDialogComponent, { backdrop: 'static', size: 'xl', }) - modal.componentInstance.dialogMode = workflow - ? EditDialogMode.EDIT - : EditDialogMode.CREATE + modal.componentInstance.dialogMode = + workflow && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE if (workflow) { // quick "deep" clone so original doesn't get modified const clone = Object.assign({}, workflow) @@ -88,6 +87,25 @@ export class WorkflowsComponent }) } + copyWorkflow(workflow: Workflow) { + const clone = Object.assign({}, workflow) + clone.id = null + clone.name = `${workflow.name} (copy)` + clone.actions = [ + ...workflow.actions.map((a) => { + a.id = null + return a + }), + ] + clone.triggers = [ + ...workflow.triggers.map((t) => { + t.id = null + return t + }), + ] + this.editWorkflow(clone, true) + } + deleteWorkflow(workflow: Workflow) { const modal = this.modalService.open(ConfirmDialogComponent, { backdrop: 'static', From d5573b933daa94b45c07e6ec8e8482e447b91935 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:09:42 -0700 Subject: [PATCH 04/74] Fix: chrome scrolling in >= 129 (#7738) --- .../app-frame/app-frame.component.scss | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.scss b/src-ui/src/app/components/app-frame/app-frame.component.scss index cdb6e3be5..7f9871b97 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.scss +++ b/src-ui/src/app/components/app-frame/app-frame.component.scss @@ -12,6 +12,9 @@ z-index: 995; /* Behind the navbar */ padding: 50px 0 0; /* Height of navbar */ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); + overflow-y: auto; + --pngx-sidebar-width: 25%; + max-width: var(--pngx-sidebar-width); .sidebar-heading .spinner-border { width: 0.8em; @@ -24,15 +27,15 @@ // These come from the col-* classes for non-slim sidebar, needed for animation @media (min-width: 768px) { - max-width: 25%; + --pngx-sidebar-width: 25%; } @media (min-width: 992px) { - max-width: 16.66666667%; + --pngx-sidebar-width: 16.66666667%; } @media (min-width: 2400px) { - max-width: 8.33333333%; + --pngx-sidebar-width: 8.33333333%; } transition: all .2s ease; @@ -109,12 +112,17 @@ main { .sidebar-slim-toggler { display: block; - position: absolute; - right: -12px; + position: fixed; + left: calc(var(--pngx-sidebar-width) - 12px); top: 60px; z-index: 996; --bs-btn-padding-x: 0.35rem; --bs-btn-padding-y: 0.125rem; + transition: all .2s ease; + } + + .sidebar.slim .sidebar-slim-toggler { + --pngx-sidebar-width: 50px !important; } } From ec370beb69b857791ff42e114647e7ef2229fb4c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:58:40 -0700 Subject: [PATCH 05/74] Enhancement: allow setting session cookie age (#7743) --- docs/configuration.md | 7 +++++++ src/paperless/settings.py | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 3530849dd..301e86fc2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -611,6 +611,13 @@ You can optionally also automatically redirect users to the SSO login with [PAPE : Only applies to regular (non-SSO) accounts. See the corresponding [django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html) +#### [`PAPERLESS_SESSION_COOKIE_AGE=`](#PAPERLESS_SESSION_COOKIE_AGE) {#PAPERLESS_SESSION_COOKIE_AGE} + +: Default login cookie expiration. Applies to regular logins if remember is enabled and always for SSO logins. See the corresponding +[django documentation](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE) + + Defaults to 1209600 (2 weeks) + ## OCR settings {#ocr} Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index ebe64ba9e..9a57719a2 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -472,7 +472,10 @@ ACCOUNT_EMAIL_VERIFICATION = os.getenv( "optional", ) -ACCOUNT_SESSION_REMEMBER = __get_boolean("PAPERLESS_ACCOUNT_SESSION_REMEMBER") +ACCOUNT_SESSION_REMEMBER = __get_boolean("PAPERLESS_ACCOUNT_SESSION_REMEMBER", "True") +SESSION_COOKIE_AGE = int( + os.getenv("PAPERLESS_SESSION_COOKIE_AGE", 60 * 60 * 24 * 7 * 3), +) if AUTO_LOGIN_USERNAME: _index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware") From 0d878f600385f6c9607445ed0eba5bb63a206af7 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:20:13 -0700 Subject: [PATCH 06/74] Documentation: fix session cookie config type --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 301e86fc2..5b0434aaf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -611,7 +611,7 @@ You can optionally also automatically redirect users to the SSO login with [PAPE : Only applies to regular (non-SSO) accounts. See the corresponding [django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html) -#### [`PAPERLESS_SESSION_COOKIE_AGE=`](#PAPERLESS_SESSION_COOKIE_AGE) {#PAPERLESS_SESSION_COOKIE_AGE} +#### [`PAPERLESS_SESSION_COOKIE_AGE=`](#PAPERLESS_SESSION_COOKIE_AGE) {#PAPERLESS_SESSION_COOKIE_AGE} : Default login cookie expiration. Applies to regular logins if remember is enabled and always for SSO logins. See the corresponding [django documentation](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE) From 15e263be68eb25c64161863efdb6fc6d2f79eb89 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:36:40 -0700 Subject: [PATCH 07/74] Enhancement: set Django SESSION_EXPIRE_AT_BROWSER_CLOSE from PAPERLESS_ACCOUNT_SESSION_REMEMBER (#7748) --- docs/configuration.md | 6 ++++-- src/paperless/settings.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5b0434aaf..57edb7c72 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -608,12 +608,14 @@ You can optionally also automatically redirect users to the SSO login with [PAPE #### [`PAPERLESS_ACCOUNT_SESSION_REMEMBER=`](#PAPERLESS_ACCOUNT_SESSION_REMEMBER) {#PAPERLESS_ACCOUNT_SESSION_REMEMBER} -: Only applies to regular (non-SSO) accounts. See the corresponding +: If false, sessions will expire at browser close, if true will use `PAPERLESS_SESSION_COOKIE_AGE` for expiration. See the corresponding [django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html) + Defaults to True + #### [`PAPERLESS_SESSION_COOKIE_AGE=`](#PAPERLESS_SESSION_COOKIE_AGE) {#PAPERLESS_SESSION_COOKIE_AGE} -: Default login cookie expiration. Applies to regular logins if remember is enabled and always for SSO logins. See the corresponding +: Login session cookie expiration. Applies if `PAPERLESS_ACCOUNT_SESSION_REMEMBER` is enabled. See the corresponding [django documentation](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE) Defaults to 1209600 (2 weeks) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 9a57719a2..46a697349 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -473,6 +473,7 @@ ACCOUNT_EMAIL_VERIFICATION = os.getenv( ) ACCOUNT_SESSION_REMEMBER = __get_boolean("PAPERLESS_ACCOUNT_SESSION_REMEMBER", "True") +SESSION_EXPIRE_AT_BROWSER_CLOSE = not ACCOUNT_SESSION_REMEMBER SESSION_COOKIE_AGE = int( os.getenv("PAPERLESS_SESSION_COOKIE_AGE", 60 * 60 * 24 * 7 * 3), ) From e1236244fe1ecb73352b0cb1de8c396523fea8af Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 22 Sep 2024 22:51:59 -0700 Subject: [PATCH 08/74] Fix sidebar mobile width --- src-ui/src/app/components/app-frame/app-frame.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.scss b/src-ui/src/app/components/app-frame/app-frame.component.scss index 7f9871b97..f5427c713 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.scss +++ b/src-ui/src/app/components/app-frame/app-frame.component.scss @@ -13,7 +13,7 @@ padding: 50px 0 0; /* Height of navbar */ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); overflow-y: auto; - --pngx-sidebar-width: 25%; + --pngx-sidebar-width: 100%; max-width: var(--pngx-sidebar-width); .sidebar-heading .spinner-border { From cd7f2b8055d0e48cd5c02afcb59bb91b8c57a044 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:29:37 -0700 Subject: [PATCH 09/74] Fix: handle overflowing dropdowns on mobile (#7758) See https://github.com/ng-bootstrap/ng-bootstrap/pull/4760 --- .../dates-dropdown.component.html | 2 +- .../dates-dropdown.component.ts | 3 +++ .../filterable-dropdown.component.html | 2 +- .../filterable-dropdown.component.ts | 3 +++ src-ui/src/app/utils/popper-options.spec.ts | 24 +++++++++++++++++++ src-ui/src/app/utils/popper-options.ts | 24 +++++++++++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src-ui/src/app/utils/popper-options.spec.ts create mode 100644 src-ui/src/app/utils/popper-options.ts diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html index 8991363d2..b9528805b 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html @@ -1,4 +1,4 @@ -
    +
    -
    +
    {{rd.name}}
    @@ -28,20 +28,19 @@
    } -
  • @@ -86,8 +87,16 @@
  • -
    {{rule.order}}
    +
    {{rule.order}}
    {{(mailAccountService.getCached(rule.account) | async)?.name}}
    +
    +
    + + +
    +
    diff --git a/src-ui/src/app/components/manage/mail/mail.component.spec.ts b/src-ui/src/app/components/manage/mail/mail.component.spec.ts index 4a134b880..14cd10944 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.spec.ts +++ b/src-ui/src/app/components/manage/mail/mail.component.spec.ts @@ -43,14 +43,15 @@ import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { SwitchComponent } from '../../common/input/switch/switch.component' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { By } from '@angular/platform-browser' const mailAccounts = [ { id: 1, name: 'account1' }, { id: 2, name: 'account2' }, ] const mailRules = [ - { id: 1, name: 'rule1', owner: 1, account: 1 }, - { id: 2, name: 'rule2', owner: 2, account: 2 }, + { id: 1, name: 'rule1', owner: 1, account: 1, enabled: true }, + { id: 2, name: 'rule2', owner: 2, account: 2, enabled: true }, ] describe('MailComponent', () => { @@ -321,4 +322,30 @@ describe('MailComponent', () => { dialog.confirmClicked.emit({ permissions: perms, merge: true }) expect(accountPatchSpy).toHaveBeenCalled() }) + + it('should update mail rule when enable is toggled', () => { + completeSetup() + const patchSpy = jest.spyOn(mailRuleService, 'patch') + const toggleInput = fixture.debugElement.query( + By.css('input[type="checkbox"]') + ) + const toastErrorSpy = jest.spyOn(toastService, 'showError') + const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + // fail first + patchSpy.mockReturnValueOnce( + throwError(() => new Error('Error getting config')) + ) + toggleInput.nativeElement.click() + expect(patchSpy).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() + // succeed second + patchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule)) + toggleInput.nativeElement.click() + patchSpy.mockReturnValueOnce( + of({ ...mailRules[0], enabled: false } as MailRule) + ) + toggleInput.nativeElement.click() + expect(patchSpy).toHaveBeenCalled() + expect(toastInfoSpy).toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/manage/mail/mail.component.ts b/src-ui/src/app/components/manage/mail/mail.component.ts index 5d00b6c13..288e8e121 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.ts +++ b/src-ui/src/app/components/manage/mail/mail.component.ts @@ -170,6 +170,21 @@ export class MailComponent this.editMailRule(clone, true) } + onMailRuleEnableToggled(rule: MailRule) { + this.mailRuleService.patch(rule).subscribe({ + next: () => { + this.toastService.showInfo( + rule.enabled + ? $localize`Rule "${rule.name}" enabled.` + : $localize`Rule "${rule.name}" disabled.` + ) + }, + error: (e) => { + this.toastService.showError($localize`Error toggling rule.`, e) + }, + }) + } + deleteMailRule(rule: MailRule) { const modal = this.modalService.open(ConfirmDialogComponent, { backdrop: 'static', diff --git a/src-ui/src/app/data/mail-rule.ts b/src-ui/src/app/data/mail-rule.ts index 2611fa3ba..7888b19e6 100644 --- a/src-ui/src/app/data/mail-rule.ts +++ b/src-ui/src/app/data/mail-rule.ts @@ -39,6 +39,8 @@ export interface MailRule extends ObjectWithPermissions { order: number + enabled: boolean + folder: string filter_from: string diff --git a/src-ui/src/app/services/rest/mail-rule.service.spec.ts b/src-ui/src/app/services/rest/mail-rule.service.spec.ts index ea84e8b86..87e21172c 100644 --- a/src-ui/src/app/services/rest/mail-rule.service.spec.ts +++ b/src-ui/src/app/services/rest/mail-rule.service.spec.ts @@ -18,6 +18,7 @@ const mail_rules = [ id: 1, account: 1, order: 1, + enabled: true, folder: 'INBOX', filter_from: null, filter_to: null, @@ -36,6 +37,7 @@ const mail_rules = [ id: 2, account: 1, order: 1, + enabled: true, folder: 'INBOX', filter_from: null, filter_to: null, @@ -54,6 +56,7 @@ const mail_rules = [ id: 3, account: 1, order: 1, + enabled: true, folder: 'INBOX', filter_from: null, filter_to: null, diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index c83ebd493..ef856fbc7 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -369,6 +369,10 @@ textarea, cursor: not-allowed; } +.cursor-pointer { + cursor: pointer; +} + ul.pagination { margin-bottom: 0; } diff --git a/src/documents/tests/test_migration_workflows.py b/src/documents/tests/test_migration_workflows.py index 403067ca6..81bb577b2 100644 --- a/src/documents/tests/test_migration_workflows.py +++ b/src/documents/tests/test_migration_workflows.py @@ -8,7 +8,7 @@ class TestMigrateWorkflow(TestMigrations): dependencies = ( ( "paperless_mail", - "0025_alter_mailaccount_owner_alter_mailrule_owner_and_more", + "0026_mailrule_enabled", ), ) diff --git a/src/paperless_mail/admin.py b/src/paperless_mail/admin.py index adec5e17c..2ff313584 100644 --- a/src/paperless_mail/admin.py +++ b/src/paperless_mail/admin.py @@ -53,7 +53,7 @@ class MailRuleAdmin(GuardedModelAdmin): } fieldsets = ( - (None, {"fields": ("name", "order", "account", "folder")}), + (None, {"fields": ("name", "order", "account", "enabled", "folder")}), ( _("Filter"), { diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index b52a2ebe4..84f97b742 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -536,6 +536,9 @@ class MailAccountHandler(LoggingMixin): ) for rule in account.rules.order_by("order"): + if not rule.enabled: + self.log.debug(f"Rule {rule}: Skipping disabled rule") + continue try: total_processed_files += self._handle_mail_rule( M, diff --git a/src/paperless_mail/migrations/0026_mailrule_enabled.py b/src/paperless_mail/migrations/0026_mailrule_enabled.py new file mode 100644 index 000000000..c10ee698c --- /dev/null +++ b/src/paperless_mail/migrations/0026_mailrule_enabled.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.1 on 2024-09-30 15:17 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "paperless_mail", + "0025_alter_mailaccount_owner_alter_mailrule_owner_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="mailrule", + name="enabled", + field=models.BooleanField(default=True, verbose_name="enabled"), + ), + ] diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py index c53b16f1f..c23ea48c7 100644 --- a/src/paperless_mail/models.py +++ b/src/paperless_mail/models.py @@ -115,6 +115,8 @@ class MailRule(document_models.ModelWithOwner): verbose_name=_("account"), ) + enabled = models.BooleanField(_("enabled"), default=True) + folder = models.CharField( _("folder"), default="INBOX", diff --git a/src/paperless_mail/serialisers.py b/src/paperless_mail/serialisers.py index 38ee9661e..9237b47de 100644 --- a/src/paperless_mail/serialisers.py +++ b/src/paperless_mail/serialisers.py @@ -74,6 +74,7 @@ class MailRuleSerializer(OwnedObjectSerializer): "id", "name", "account", + "enabled", "folder", "filter_from", "filter_to", diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index c12b54ffe..9078335a6 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -1388,6 +1388,41 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 0) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) + def test_disabled_rule(self): + """ + GIVEN: + - Mail rule is disabled + WHEN: + - Mail account is handled + THEN: + - Should not process any messages + """ + account = MailAccount.objects.create( + name="test", + imap_server="", + username="admin", + password="secret", + ) + MailRule.objects.create( + name="testrule", + account=account, + action=MailRule.MailAction.MARK_READ, + enabled=False, + ) + + self.mail_account_handler.handle_mail_account(account) + self.mailMocker.apply_mail_actions() + + self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) + self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 2) + + self.mail_account_handler.handle_mail_account(account) + self.mailMocker.apply_mail_actions() + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), + 2, + ) # still 2 + class TestManagementCommand(TestCase): @mock.patch( From e3c7c925ddc6857598c26ab19b14390da7974f85 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:44:02 -0700 Subject: [PATCH 24/74] Enhancement: workflow overview toggle enable button (#7818) --- src-ui/messages.xlf | 39 ++++++++++++++----- .../manage/workflows/workflows.component.html | 17 +++++--- .../workflows/workflows.component.spec.ts | 23 +++++++++++ .../manage/workflows/workflows.component.ts | 17 ++++++++ 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index e0d30bdc2..f5d270376 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -1565,11 +1565,11 @@ src/app/components/manage/workflows/workflows.component.html - 41 + 48 src/app/components/manage/workflows/workflows.component.html - 52 + 59 @@ -2470,11 +2470,11 @@ src/app/components/manage/workflows/workflows.component.html - 40 + 47 src/app/components/manage/workflows/workflows.component.html - 49 + 56 @@ -3700,7 +3700,7 @@ src/app/components/manage/workflows/workflows.component.html - 30 + 34 @@ -5059,11 +5059,11 @@ src/app/components/manage/workflows/workflows.component.html - 42 + 49 src/app/components/manage/workflows/workflows.component.html - 57 + 64 @@ -7533,7 +7533,7 @@ src/app/components/manage/workflows/workflows.component.html - 30 + 34 @@ -7927,7 +7927,7 @@ No workflows defined. src/app/components/manage/workflows/workflows.component.html - 66 + 73 @@ -7972,6 +7972,27 @@ 128 + + Enabled workflow + + src/app/components/manage/workflows/workflows.component.ts + 139 + + + + Disabled workflow + + src/app/components/manage/workflows/workflows.component.ts + 140 + + + + Error toggling workflow. + + src/app/components/manage/workflows/workflows.component.ts + 146 + + Not Found diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.html b/src-ui/src/app/components/manage/workflows/workflows.component.html index 1e83efd36..ddb8a8654 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.html +++ b/src-ui/src/app/components/manage/workflows/workflows.component.html @@ -15,9 +15,9 @@
  • Name
    -
    Sort order
    +
    Sort order
    Status
    -
    Triggers
    +
    Triggers
    Actions
  • @@ -26,9 +26,16 @@
  • -
    {{workflow.order}}
    -
    @if(workflow.enabled) { Enabled } @else { Disabled }
    -
    {{getTypesList(workflow)}}
    +
    {{workflow.order}}
    +
    +
    + + +
    +
    +
    {{getTypesList(workflow)}}
    diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts index 0bccbad2d..9d92d9ba7 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts @@ -211,4 +211,27 @@ describe('WorkflowsComponent', () => { editDialog.confirmClicked.emit() expect(reloadSpy).toHaveBeenCalled() }) + + it('should update workflow when enable is toggled', () => { + const patchSpy = jest.spyOn(workflowService, 'patch') + const toggleInput = fixture.debugElement.query( + By.css('input[type="checkbox"]') + ) + const toastErrorSpy = jest.spyOn(toastService, 'showError') + const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + // fail first + patchSpy.mockReturnValueOnce( + throwError(() => new Error('Error getting config')) + ) + toggleInput.nativeElement.click() + expect(patchSpy).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() + // succeed second + patchSpy.mockReturnValueOnce(of(workflows[0])) + toggleInput.nativeElement.click() + patchSpy.mockReturnValueOnce(of({ ...workflows[0], enabled: false })) + toggleInput.nativeElement.click() + expect(patchSpy).toHaveBeenCalled() + expect(toastInfoSpy).toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.ts b/src-ui/src/app/components/manage/workflows/workflows.component.ts index 92b421e9f..592dd3efe 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.ts @@ -130,4 +130,21 @@ export class WorkflowsComponent }) }) } + + onWorkflowEnableToggled(workflow: Workflow) { + this.workflowService.patch(workflow).subscribe({ + next: () => { + this.toastService.showInfo( + workflow.enabled + ? $localize`Enabled workflow` + : $localize`Disabled workflow` + ) + this.workflowService.clearCache() + this.reload() + }, + error: (e) => { + this.toastService.showError($localize`Error toggling workflow.`, e) + }, + }) + } } From d61e3b1af5b95f7ff1b83a69646c1f31016a094c Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:53:44 -0700 Subject: [PATCH 25/74] Chore: Upgrades OCRMyPDF to v16 (#7815) --- .pre-commit-config.yaml | 1 - Pipfile | 2 +- Pipfile.lock | 98 ++++++++++++++------ src/paperless_mail/tests/test_parsers.py | 2 + src/paperless_tika/tests/test_tika_parser.py | 26 ++++-- 5 files changed, 91 insertions(+), 38 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb38eaf26..a20d4ca8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,7 +62,6 @@ repos: rev: v6.2.1 hooks: - id: beautysh - language_version: '3.10' additional_dependencies: - setuptools args: diff --git a/Pipfile b/Pipfile index 5061a433f..a872e1184 100644 --- a/Pipfile +++ b/Pipfile @@ -35,7 +35,7 @@ inotifyrecursive = "~=0.3" langdetect = "*" mysqlclient = "*" nltk = "*" -ocrmypdf = "~=15.4" +ocrmypdf = "~=16.5" pathvalidate = "*" pdf2image = "*" psycopg = {version = "*", extras = ["c"]} diff --git a/Pipfile.lock b/Pipfile.lock index 8d2f5d8b4..47622a94f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e2c4bfb1db243ebdfd0a4ca4a1709c35599e4f3999187870f268416aa01a225f" + "sha256": "1be8ddf875b6aa77fcf61f5c065c9dc3941cad4b9285ce64da60b5684357dade" }, "pipfile-spec": 6, "requires": {}, @@ -261,14 +261,6 @@ "markers": "python_version >= '3.8'", "version": "==4.2.0" }, - "chardet": { - "hashes": [ - "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", - "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970" - ], - "markers": "python_version >= '3.7'", - "version": "==5.2.0" - }, "charset-normalizer": { "hashes": [ "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", @@ -1212,12 +1204,12 @@ }, "ocrmypdf": { "hashes": [ - "sha256:13fd388035b5f4bb673bff570cfc2cf72e51168646d5401de9e48ca355917c6d", - "sha256:4696c81cc5b5d64f31ccfe685d10baeb69b42bb0974acddf292d8cf9d97605c3" + "sha256:9222b1b0818b65c891559b84efab2e84434c71149b3aaaa6dc654457e0b66b14", + "sha256:cd96bddfb3a986be7bf7857757919332e1db5dab780eb7b321fdea38f60127ac" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==15.4.4" + "markers": "python_version >= '3.10'", + "version": "==16.5.0" }, "packaging": { "hashes": [ @@ -1244,7 +1236,7 @@ "index": "pypi", "version": "==1.17.0" }, - "pdfminer.six": { + "pdfminer-six": { "hashes": [ "sha256:c631a46d5da957a9ffe4460c5dce21e8431dabb615fee5f9f4400603a58d95a6", "sha256:f4f70e74174b4b3542fcb8406a210b6e2e27cd0f0b5fd04534a8cc0d8951e38c" @@ -1252,6 +1244,64 @@ "markers": "python_version >= '3.8'", "version": "==20240706" }, + "pi-heif": { + "hashes": [ + "sha256:00a6d72ba2cc1477c8a909bfbbac4f5d931a25a88979077b231b76e7b9c80ba6", + "sha256:054cd3544e421b342b15b5eb8db4de222a09ca3ae441f4fa5943f80d9e65c5d6", + "sha256:0962b4cd828ad1ae94f9cd8e95ed0741cddcd19082cb97d5b69bfe1ac6623eb9", + "sha256:0a690159607beaa6712f2c8abaa5168a22314d18f00a617d691548f5acba8070", + "sha256:0d5dd431dbf7be88267fbfb08623bcf2d16628cdcbc898bcc0e05412dc43fd26", + "sha256:1159f54d76b860cc27753c9925e2923959d8b5277372db946cb1078fa11ed1ea", + "sha256:18d113c14fecadb90c3d8838240120e6f93671618eb96d776f994b314f1f858c", + "sha256:24ca403e556c84ce0e36ea1477530f7854e71c2523eb1a97c91d5d9ce8bbc548", + "sha256:286a5d2b5036cf3da8f1a2e1ad54044aaabe4d46b178057323f5a6ce19417741", + "sha256:2b892ebc898ca32c1a1ec9e72658c0d14de5ac31c1bd61a8aa66dc645080e32f", + "sha256:2c912219964dc864e1454ab4f43d97cbf6a88d065410a16936e7c59b1290a7da", + "sha256:34725b542bd2737be7e7909fff1fb6d39760d3d395a36ce6fae5280e88ba94a6", + "sha256:3529f904f51594a613759ab610799ce34b615339d67e642843eec1ac7868814d", + "sha256:3c09d22ed75200372b8102debf4ba69d8f63c595870505b9188d6c9a9b48e1f2", + "sha256:3fa5366b2f555b6b3a56b09aa74f178a040edb174b29060d8d56c03eea154e43", + "sha256:45d360c3a056d9c81b0480a546f291bbc53caf70705f3a49d082e728735ed4ae", + "sha256:4d88aba685051131f103a7afc428412abd7d09640719635f8880898b0e7aec97", + "sha256:4ecb9031ad1cb7eed1591cba95420964557cff8fc63bab9bdc204d53301e502f", + "sha256:5254dc3121d2a38036beae631aae620d0c942f03973ec134ae9827b60e7d5c0b", + "sha256:5424435551e606e1ac515de46a2b1c6d8e82c7a89473bb7cf9398368f051d675", + "sha256:571d69be0088336c4251d7301f3fdc0fecab45e38286e71a23e64814489c5a15", + "sha256:573602d8c68f4ff93c4d35439d7566b3f2d4ab774925367aece20f9cd0ba243d", + "sha256:64ed341f91763e29096b0ddb38b50d13879d06039889d458fc7dac6d5c03dd80", + "sha256:6541a05177c3d8f00e56f4cc8ee9c681eb25fcdc917065acbc426847eb8aea97", + "sha256:6c7a28547e3f1e2f43b395d2764f693fcfa4eb8a4da0d5815c7eb3eeda745fbb", + "sha256:71309d2a632c0b8716ccbbb9e413ee28b8439967c45c92de68888fe4acf80244", + "sha256:742560127423bd179605325a41322df800ca02df768e872bfe189fe371f61578", + "sha256:74d4b07f0589df9fac138ecbcccd248217a12bbebd3443153158d7f54522e257", + "sha256:79969f90a5a01b9a82b18bb0667392da733790585531b3183b7f375b9e88dbcd", + "sha256:7a9a95f54cb3a473005572f7309666b71d03c1764134b2df0ed796744c7aa069", + "sha256:7acdd41dc72c01c1f2cfd91624a1c102ecc324fff6a501ab981c6f803f673b1b", + "sha256:7e0c3286f106f2d22d394b844c0e015f132567d70b31fef6d3cc846b8fe9dbc6", + "sha256:83548aa70e44fef865c2b2575ed949f2e6eba756b114ca6ad525ef56b5449d57", + "sha256:86f7aad733292fea8a2869814117caf11ed424731bd90fe1693b2ccbfcc6bfed", + "sha256:886fbbda898559eba0843feca17e6c7e43c13336404817c6d07a01d4955c3d33", + "sha256:8d0a7529225f1a25231d8f2cfd39f722c31e5396581eeeaa7a30793188e8b4f7", + "sha256:9ff516f9f5118a8f2e47531611324e6a07848e4f1f17c5df485de734e50dee7e", + "sha256:a4b3690f03636944b13ab313d21ee90a46d5fa35a15d884563b0ff400b813042", + "sha256:aac4fc247139081b30581cadbea00bb4c4fb7274140eaa1147e22bcf7ece7525", + "sha256:ad3f54dcc54a4c2ed1c58a135375330fe7b2ba2c2a8a816d3296c12e9d8c284c", + "sha256:b2af8ac6bd93e5df02b9f292a10664524844f37b39079e55aa9ef5857a3b0a22", + "sha256:c5bded35d1cefb594f6ce9d775e3e6b750a32926779f7b496f0f8d4992db09e1", + "sha256:cab6f7a00ccbcc3087d400a544e62ef30eff6339cf0d600588b92b1e7ca49d96", + "sha256:ccd611653581f39c77ab8222a660e471e724d8f7c6f4e50760b10ce06769d9d8", + "sha256:cfa979043be0d4ad1b37f6794fdff010cf69e5ada1ef74eef4a5b3983d3b8881", + "sha256:d7dc682acccd81857fd4b5849ebe7b9504e11eab493ffa0905ea25eaf5fb0f93", + "sha256:e568a323548896848489035c5bb2e4de13df07fbdbd33831b165ff545066b97f", + "sha256:f19d8cdffbc5e8e9f3676839c8632ffd161d17f84f614cad9b98a58e27ffd3a7", + "sha256:f1b7c4daeaffb235e73fc54132f4aa8bcb229dcb463ac0b4def9e1aee5793165", + "sha256:f792a278335c278d2c092a62aaad3a7362021f9341f988b1b8b3ca4783651e49", + "sha256:fae39eec07f4b477c582ddd75d38610553c1b6d19cd6ce4a3ded4c7e0ee029ac", + "sha256:fe0e424d08d59c5a1d74dfa7239b40a935b5a526305ebecd2c27755aa3442225" + ], + "markers": "python_version >= '3.8'", + "version": "==0.18.0" + }, "pikepdf": { "hashes": [ "sha256:01be001988ce0f6a5a89319f37fc14f27df75c4e332222ed8e993d14405acb02", @@ -1788,14 +1838,6 @@ "markers": "python_version >= '3.8'", "version": "==2024.9.11" }, - "reportlab": { - "hashes": [ - "sha256:6e4d86647b8bfd772f475a58f9b0dcba4b340b1969f0db36333089f6ca9ab362", - "sha256:a00b57292e156a7bda84edf31d60c25578153076c8fb96331d0c59eddda052c8" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==4.2.4" - }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", @@ -3227,12 +3269,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:1843c5171ad6b489550aeaf7358e5b7128cc03ddcf0fb4d91d19aa1e691a63b8", - "sha256:d4779051d52ba9f1e7e344b34de95449c7c366c212b388e4a2db9a3db043c228" + "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b", + "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.38" + "version": "==9.5.39" }, "mkdocs-material-extensions": { "hashes": [ @@ -3528,12 +3570,12 @@ }, "pytest-httpx": { "hashes": [ - "sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c", - "sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a" + "sha256:685d93ce5e5edb5e52310b72342cdc190bebf83aab058328943dd8bd8f6ac790", + "sha256:7807647e8254e5cff79bf2041ae272449ce915d3cf1bbecaa581c384163adb87" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==0.30.0" + "version": "==0.32.0" }, "pytest-mock": { "hashes": [ diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index a0baa4821..e8186ea0f 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -497,6 +497,7 @@ class TestParser: assert mail_parser.archive_path is not None + @pytest.mark.httpx_mock(can_send_already_matched_responses=True) def test_generate_pdf_html_email( self, httpx_mock: HTTPXMock, @@ -575,6 +576,7 @@ class TestParser: with pytest.raises(ParseError): mail_parser.parse(html_email_file, "message/rfc822") + @pytest.mark.httpx_mock(can_send_already_matched_responses=True) def test_generate_pdf_html_email_merge_failure( self, httpx_mock: HTTPXMock, diff --git a/src/paperless_tika/tests/test_tika_parser.py b/src/paperless_tika/tests/test_tika_parser.py index 6b048f252..cebae2486 100644 --- a/src/paperless_tika/tests/test_tika_parser.py +++ b/src/paperless_tika/tests/test_tika_parser.py @@ -5,7 +5,6 @@ from pathlib import Path import pytest from httpx import codes -from httpx._multipart import DataField from pytest_django.fixtures import SettingsWrapper from pytest_httpx import HTTPXMock @@ -128,11 +127,22 @@ class TestTikaParser: tika_parser.convert_to_pdf(sample_odt_file, None) request = httpx_mock.get_request() - found = False - for field in request.stream.fields: - if isinstance(field, DataField) and field.name == "pdfa": - assert field.value == expected_form_value - found = True - assert found, "pdfFormat was not found" - httpx_mock.reset(assert_all_responses_were_requested=False) + expected_field_name = "pdfa" + + content_type = request.headers["Content-Type"] + assert "multipart/form-data" in content_type + + boundary = content_type.split("boundary=")[1] + + parts = request.content.split(f"--{boundary}".encode()) + + form_field_found = any( + f'name="{expected_field_name}"'.encode() in part + and expected_form_value.encode() in part + for part in parts + ) + + assert form_field_found + + httpx_mock.reset() From e92ae819983b9992c2c087982925ba6a91b0a165 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:43:28 -0700 Subject: [PATCH 26/74] Chore(deps): Bump the frontend-angular-dependencies group (#7825) Bumps the frontend-angular-dependencies group in /src-ui with 21 updates: | Package | From | To | | --- | --- | --- | | [@angular/cdk](https://github.com/angular/components) | `18.2.2` | `18.2.6` | | [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `18.2.2` | `18.2.6` | | [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `18.2.2` | `18.2.6` | | [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `18.2.2` | `18.2.6` | | [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `18.2.2` | `18.2.6` | | [@angular/localize](https://github.com/angular/angular) | `18.2.2` | `18.2.6` | | [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `18.2.2` | `18.2.6` | | [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `18.2.2` | `18.2.6` | | [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `18.2.2` | `18.2.6` | | [@ng-select/ng-select](https://github.com/ng-select/ng-select) | `13.7.0` | `13.9.0` | | [ng2-pdf-viewer](https://github.com/VadimDez/ng2-pdf-viewer) | `10.3.0` | `10.3.1` | | [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` | | [@angular-devkit/core](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` | | [@angular-devkit/schematics](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` | | [@angular-eslint/builder](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/builder) | `18.3.0` | `18.3.1` | | [@angular-eslint/eslint-plugin](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin) | `18.3.0` | `18.3.1` | | [@angular-eslint/eslint-plugin-template](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin-template) | `18.3.0` | `18.3.1` | | [@angular-eslint/schematics](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/schematics) | `18.3.0` | `18.3.1` | | [@angular-eslint/template-parser](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/template-parser) | `18.3.0` | `18.3.1` | | [@angular/cli](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` | | [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `18.2.2` | `18.2.6` | Updates `@angular/cdk` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/components/releases) - [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/components/compare/18.2.2...18.2.6) Updates `@angular/common` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/common) Updates `@angular/compiler` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/compiler) Updates `@angular/core` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/core) Updates `@angular/forms` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/forms) Updates `@angular/localize` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/compare/18.2.2...18.2.6) Updates `@angular/platform-browser` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/platform-browser) Updates `@angular/platform-browser-dynamic` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/platform-browser-dynamic) Updates `@angular/router` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/router) Updates `@ng-select/ng-select` from 13.7.0 to 13.9.0 - [Release notes](https://github.com/ng-select/ng-select/releases) - [Changelog](https://github.com/ng-select/ng-select/blob/master/CHANGELOG.md) - [Commits](https://github.com/ng-select/ng-select/compare/v13.7.0...v13.9.0) Updates `ng2-pdf-viewer` from 10.3.0 to 10.3.1 - [Release notes](https://github.com/VadimDez/ng2-pdf-viewer/releases) - [Changelog](https://github.com/VadimDez/ng2-pdf-viewer/blob/master/CHANGELOG.md) - [Commits](https://github.com/VadimDez/ng2-pdf-viewer/compare/10.3.0...10.3.1) Updates `@angular-devkit/build-angular` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/18.2.2...18.2.6) Updates `@angular-devkit/core` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/18.2.2...18.2.6) Updates `@angular-devkit/schematics` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/18.2.2...18.2.6) Updates `@angular-eslint/builder` from 18.3.0 to 18.3.1 - [Release notes](https://github.com/angular-eslint/angular-eslint/releases) - [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/builder/CHANGELOG.md) - [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/builder) Updates `@angular-eslint/eslint-plugin` from 18.3.0 to 18.3.1 - [Release notes](https://github.com/angular-eslint/angular-eslint/releases) - [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/eslint-plugin) Updates `@angular-eslint/eslint-plugin-template` from 18.3.0 to 18.3.1 - [Release notes](https://github.com/angular-eslint/angular-eslint/releases) - [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/CHANGELOG.md) - [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/eslint-plugin-template) Updates `@angular-eslint/schematics` from 18.3.0 to 18.3.1 - [Release notes](https://github.com/angular-eslint/angular-eslint/releases) - [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/schematics/CHANGELOG.md) - [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/schematics) Updates `@angular-eslint/template-parser` from 18.3.0 to 18.3.1 - [Release notes](https://github.com/angular-eslint/angular-eslint/releases) - [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/template-parser/CHANGELOG.md) - [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/template-parser) Updates `@angular/cli` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/18.2.2...18.2.6) Updates `@angular/compiler-cli` from 18.2.2 to 18.2.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/18.2.6/packages/compiler-cli) --- updated-dependencies: - dependency-name: "@angular/cdk" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/common" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/compiler" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/core" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/forms" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/localize" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/platform-browser" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/platform-browser-dynamic" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/router" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@ng-select/ng-select" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: frontend-angular-dependencies - dependency-name: ng2-pdf-viewer dependency-type: direct:production update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-devkit/build-angular" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-devkit/core" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-devkit/schematics" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-eslint/builder" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-eslint/eslint-plugin-template" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-eslint/schematics" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular-eslint/template-parser" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies - dependency-name: "@angular/compiler-cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-angular-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src-ui/package-lock.json | 1787 +++++++++++++++++--------------------- src-ui/package.json | 38 +- 2 files changed, 801 insertions(+), 1024 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 21251cca3..7871bd71e 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -9,23 +9,23 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { - "@angular/cdk": "^18.2.2", - "@angular/common": "~18.2.2", - "@angular/compiler": "~18.2.2", - "@angular/core": "~18.2.2", - "@angular/forms": "~18.2.2", - "@angular/localize": "~18.2.2", - "@angular/platform-browser": "~18.2.2", - "@angular/platform-browser-dynamic": "~18.2.2", - "@angular/router": "~18.2.2", + "@angular/cdk": "^18.2.6", + "@angular/common": "~18.2.6", + "@angular/compiler": "~18.2.6", + "@angular/core": "~18.2.6", + "@angular/forms": "~18.2.6", + "@angular/localize": "~18.2.6", + "@angular/platform-browser": "~18.2.6", + "@angular/platform-browser-dynamic": "~18.2.6", + "@angular/router": "~18.2.6", "@ng-bootstrap/ng-bootstrap": "^17.0.1", - "@ng-select/ng-select": "^13.7.0", + "@ng-select/ng-select": "^13.9.0", "@ngneat/dirty-check-forms": "^3.0.3", "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "file-saver": "^2.0.5", "mime-names": "^1.0.0", - "ng2-pdf-viewer": "^10.3.0", + "ng2-pdf-viewer": "^10.3.1", "ngx-bootstrap-icons": "^1.9.3", "ngx-color": "^9.0.0", "ngx-cookie-service": "^18.0.0", @@ -40,14 +40,14 @@ "@angular-builders/custom-webpack": "^18.0.0", "@angular-builders/jest": "^18.0.0", "@angular-devkit/build-angular": "^18.2.2", - "@angular-devkit/core": "^18.2.2", - "@angular-devkit/schematics": "^18.2.2", - "@angular-eslint/builder": "18.3.0", - "@angular-eslint/eslint-plugin": "18.3.0", - "@angular-eslint/eslint-plugin-template": "18.3.0", - "@angular-eslint/schematics": "18.3.0", - "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "~18.2.2", + "@angular-devkit/core": "^18.2.6", + "@angular-devkit/schematics": "^18.2.6", + "@angular-eslint/builder": "18.3.1", + "@angular-eslint/eslint-plugin": "18.3.1", + "@angular-eslint/eslint-plugin-template": "18.3.1", + "@angular-eslint/schematics": "18.3.1", + "@angular-eslint/template-parser": "18.3.1", + "@angular/cli": "~18.2.6", "@angular/compiler-cli": "~18.2.2", "@codecov/webpack-plugin": "^1.0.1", "@playwright/test": "^1.46.1", @@ -234,12 +234,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.2.tgz", - "integrity": "sha512-LPRl9jhcf0NgshaL6RoUy1uL/cAyNt7oxctoZ9EHUu8eh5E9W/jZGhVowjOLpirwqYhmEzKJJIeS49Ssqs3RQg==", + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", + "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.2", + "@angular-devkit/core": "18.2.6", "rxjs": "7.8.1" }, "engines": { @@ -249,16 +249,16 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.2.tgz", - "integrity": "sha512-7HEnTN2T1jnjuItXKcApOsoYGgfou4+POju3ZbwIQukDZ3B2COskvQkVTxqPNrQ0ZjT2mxZYoVlmGW9M+7N25g==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.6.tgz", + "integrity": "sha512-u12cJZttgs5j7gICHWSmcaTCu0EFXEzKqI8nkYCwq2MtuJlAXiMQSXYuEP9OU3Go4vMAPtQh2kShyOWCX5b4EQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.2", - "@angular-devkit/build-webpack": "0.1802.2", - "@angular-devkit/core": "18.2.2", - "@angular/build": "18.2.2", + "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/build-webpack": "0.1802.6", + "@angular-devkit/core": "18.2.6", + "@angular/build": "18.2.6", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -269,7 +269,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.2", + "@ngtools/webpack": "18.2.6", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -309,10 +309,10 @@ "terser": "5.31.6", "tree-kill": "1.2.2", "tslib": "2.6.3", - "vite": "5.4.0", + "vite": "5.4.6", "watchpack": "2.4.1", "webpack": "5.94.0", - "webpack-dev-middleware": "7.3.0", + "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" @@ -376,6 +376,89 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular/build": { + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.6.tgz", + "integrity": "sha512-TQzX6Mi7uXFvmz7+OVl4Za7WawYPcx+B5Ewm6IY/DdMyB9P/Z4tbKb1LO+ynWUXYwm7avXo6XQQ4m5ArDY5F/A==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.6", + "@babel/core": "7.25.2", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.24.7", + "@inquirer/confirm": "3.1.22", + "@vitejs/plugin-basic-ssl": "1.1.0", + "browserslist": "^4.23.0", + "critters": "0.0.24", + "esbuild": "0.23.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "listr2": "8.2.4", + "lmdb": "3.0.13", + "magic-string": "0.30.11", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "rollup": "4.22.4", + "sass": "1.77.6", + "semver": "7.6.3", + "vite": "5.4.6", + "watchpack": "2.4.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/aix-ppc64": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", @@ -744,17 +827,219 @@ "node": ">=18" } }, - "node_modules/@angular-devkit/build-angular/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { "version": "0.23.0", @@ -762,7 +1047,6 @@ "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", "dev": true, "hasInstallScript": true, - "optional": true, "bin": { "esbuild": "bin/esbuild" }, @@ -796,21 +1080,6 @@ "@esbuild/win32-x64": "0.23.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", @@ -827,22 +1096,39 @@ "node": ">=10" } }, - "node_modules/@angular-devkit/build-angular/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "node_modules/@angular-devkit/build-angular/node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" } }, "node_modules/@angular-devkit/build-angular/node_modules/tslib": { @@ -852,12 +1138,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.2.tgz", - "integrity": "sha512-Pj+YmKh0nJOKl6QAsqYh3SqfuVJrFqjyp5WrG9BgfsMD9GCMD+5teMHNYJlp+vG/C8e7VdZp4rqOon8K9Xn4Mw==", + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.6.tgz", + "integrity": "sha512-JMLcXFaitJplwZMKkqhbYirINCRD6eOPZuIGaIOVynXYGWgvJkLT9t5C2wm9HqSLtp1K7NcYG2Y7PtTVR4krnQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.2", + "@angular-devkit/architect": "0.1802.6", "rxjs": "7.8.1" }, "engines": { @@ -871,9 +1157,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.2.tgz", - "integrity": "sha512-Zz0tGptI/QQnUBDdp+1G5wGwQWMjpfe2oO+UohkrDVgFS71yVj4VDnOy51kMTxBvzw+36evTgthPpmzqPIfxBw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", + "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", "dev": true, "dependencies": { "ajv": "8.17.1", @@ -915,12 +1201,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.2.tgz", - "integrity": "sha512-PU6+3nX+gQ3gofR7BGwXuvNUNeeV2raURaZjlPfGpBqjyTBxukMV71QsTTWptAZT4WibCWkTFp6X1gvsOGbjMg==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.6.tgz", + "integrity": "sha512-uIttrQ2cQ2PWAFFVPeCoNR8xvs7tPJ2i8gzqsIwYdge107xDC6u9CqfgmBqPDSFpWj+IiC2Jwcm8Z4HYKU4+7A==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.2", + "@angular-devkit/core": "18.2.6", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -933,9 +1219,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.3.0.tgz", - "integrity": "sha512-httEQyqyBw3+0CRtAa7muFxHrauRfkEfk/jmrh5fn2Eiu+I53hAqFPgrwVi1V6AP/kj2zbAiWhd5xM3pMJdoRQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.3.1.tgz", + "integrity": "sha512-cPc7Ye9zDs5M4i+feL6vob+mh7yX5vxvOS5KQIhneUrp5e9D+IGuNFMmBLlOPpmklSc9XJBtuvI5Zjuh4z1ETw==", "dev": true, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", @@ -943,19 +1229,19 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.0.tgz", - "integrity": "sha512-v/59FxUKnMzymVce99gV43huxoqXWMb85aKvzlNvLN+ScDu6ZE4YMiTQNpfapVL2lkxhs0uwB3jH17EYd5TcsA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.1.tgz", + "integrity": "sha512-sikmkjfsXPpPTku1aQkQ1MNNEKGBgGGRvUN/WeNS9dhCJ4dxU3O7dZctt1aQWj+W3nbuUtDiimAWF5fZHGFE2Q==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.3.0.tgz", - "integrity": "sha512-Vl7gfPMXxvtHTjYdlzR161aj5xrqW6T57wd8ToQ7Gqzm0qHGfY6kE4SQobUa2LCYckTNSlv+zXe48C4ah/dSjw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.3.1.tgz", + "integrity": "sha512-MP4Nm+SHboF8KdnN0KpPEGAaTTzDLPm3+S/4W3Mg8onqWCyadyd4mActh9mK/pvCj8TVlb/SW1zeTtdMYhwonw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", - "@angular-eslint/utils": "18.3.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -964,13 +1250,13 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.3.0.tgz", - "integrity": "sha512-ddR/qwYbUeq9IpyVKrPbfZyRBTy6V8uc5I0JcBKttQ4CZ4joXhqsVgWFsI+JAMi8E66uNj1VC7NuKCOjDINv2Q==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.3.1.tgz", + "integrity": "sha512-hBJ3+f7VSidvrtYaXH7Vp0sWvblA9jLK2c6uQzhYGWdEDUcTg7g7VI9ThW39WvMbHqkyzNE4PPOynK69cBEDGg==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", - "@angular-eslint/utils": "18.3.0", + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1", "aria-query": "5.3.0", "axobject-query": "4.1.0" }, @@ -980,42 +1266,14 @@ "typescript": "*" } }, - "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@angular-eslint/utils": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.0.tgz", - "integrity": "sha512-sCrkHkpxBJZLuCikdboZoawCfc2UgbJv+T14tu2uQCv+Vwzeadnu04vkeY2vTkA8GeBdBij/G9/N/nvwmwVw3g==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/eslint-plugin/node_modules/@angular-eslint/utils": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.0.tgz", - "integrity": "sha512-sCrkHkpxBJZLuCikdboZoawCfc2UgbJv+T14tu2uQCv+Vwzeadnu04vkeY2vTkA8GeBdBij/G9/N/nvwmwVw3g==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, "node_modules/@angular-eslint/schematics": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.3.0.tgz", - "integrity": "sha512-rQ4DEWwf3f5n096GAK6JvXD0SRzRJ52WRaIyKg8MMkk6qvUDfZI8seOkcbjDtZoIe6Ds7DfqSfJgNVte75qvPQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.3.1.tgz", + "integrity": "sha512-BTsQHDu7LjvXannJTb5BqMPCFIHRNN94eRyb60VfjJxB/ZFtsbAQDFFOi5lEZsRsd4mBeUMuL9mW4IMcPtUQ9Q==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "18.3.0", - "@angular-eslint/eslint-plugin-template": "18.3.0", + "@angular-eslint/eslint-plugin": "18.3.1", + "@angular-eslint/eslint-plugin-template": "18.3.1", "ignore": "5.3.2", "semver": "7.6.3", "strip-json-comments": "3.1.1" @@ -1026,12 +1284,12 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.3.0.tgz", - "integrity": "sha512-1mUquqcnugI4qsoxcYZKZ6WMi6RPelDcJZg2YqGyuaIuhWmi3ZqJZLErSSpjP60+TbYZu7wM8Kchqa1bwJtEaQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.3.1.tgz", + "integrity": "sha512-JUUkfWH1G+u/Uk85ZYvJSt/qwN/Ko+jlXFtzBEcknJZsTWTwBcp36v77gPZe5FmKSziJZpyPUd+7Kiy6tuSCTw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", + "@angular-eslint/bundled-angular-compiler": "18.3.1", "eslint-scope": "^8.0.2" }, "peerDependencies": { @@ -1039,485 +1297,24 @@ "typescript": "*" } }, - "node_modules/@angular/build": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.2.tgz", - "integrity": "sha512-okaDdTMXnDhvnnnih6rPQnexL6htfEAPr19bB1Ci9d31gEjVuKZCjlcw2sPZ6BUyilwC9nZlCI5vbH1Ljf6mzA==", + "node_modules/@angular-eslint/utils": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.1.tgz", + "integrity": "sha512-sd9niZI7h9H2FQ7OLiQsLFBhjhRQTASh+Q0+4+hyjv9idbSHBJli8Gsi2fqj9zhtMKpAZFTrWzuLUpubJ9UYbA==", "dev": true, "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.2", - "@babel/core": "7.25.2", - "@babel/helper-annotate-as-pure": "7.24.7", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-syntax-import-attributes": "7.24.7", - "@inquirer/confirm": "3.1.22", - "@vitejs/plugin-basic-ssl": "1.1.0", - "browserslist": "^4.23.0", - "critters": "0.0.24", - "esbuild": "0.23.0", - "fast-glob": "3.3.2", - "https-proxy-agent": "7.0.5", - "listr2": "8.2.4", - "lmdb": "3.0.13", - "magic-string": "0.30.11", - "mrmime": "2.0.0", - "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.2", - "piscina": "4.6.1", - "rollup": "4.20.0", - "sass": "1.77.6", - "semver": "7.6.3", - "vite": "5.4.0", - "watchpack": "2.4.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1" }, "peerDependencies": { - "@angular/compiler-cli": "^18.0.0", - "@angular/localize": "^18.0.0", - "@angular/platform-server": "^18.0.0", - "@angular/service-worker": "^18.0.0", - "less": "^4.2.0", - "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.4 <5.6" - }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tailwindcss": { - "optional": true - } - } - }, - "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" } }, "node_modules/@angular/cdk": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.2.tgz", - "integrity": "sha512-+u7ZcMA24WO03vDzlBJJWq+okZLFDeW9JrtHzrdiT09FDt4sdUp+7PddXaZcRHIXjJL+CaCLQ6slaqPNEufqgg==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.6.tgz", + "integrity": "sha512-Gfq/iv4zhlKYpdQkDaBRwxI71NHNUHM1Cs1XhnZ0/oFct5HXvSv1RHRGTKqBJLLACaAPzZKXJ/UglLoyO5CNiQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1531,17 +1328,17 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.2.tgz", - "integrity": "sha512-HVVaMxnbID0q+V3KE+JqzGbPHcBUFo1RKhBZ/jxY7USZNzgtyYbRc0IYqPWNdr99UT5QefTJrjVazJo1nqQZvQ==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.6.tgz", + "integrity": "sha512-tdXsnV/w+Rgu8q0zFsLU5L9ImTVqrTol1vppHaQkJ/vuoHy+s8ZEbBqhVrO/ffosNb2xseUybGYvqMS4zkNQjg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.2", - "@angular-devkit/core": "18.2.2", - "@angular-devkit/schematics": "18.2.2", + "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/core": "18.2.6", + "@angular-devkit/schematics": "18.2.6", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.2", + "@schematics/angular": "18.2.6", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -1564,9 +1361,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.2.tgz", - "integrity": "sha512-AQe4xnnNNch/sXRnV82C8FmhijxPATKfPGojC2qbAG2o6VkWKgt5Lbj0O8WxvSIOS5Syedv+O2kLY/JMGWHNtw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.6.tgz", + "integrity": "sha512-89793ow+wrI1c7C6kyMbnweLNIZHzXthosxAEjipRZGBrqBYjvTtkE45Fl+5yBa3JO7bAhyGkUnEoyvWtZIAEA==", "dependencies": { "tslib": "^2.3.0" }, @@ -1574,14 +1371,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.2", + "@angular/core": "18.2.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.2.tgz", - "integrity": "sha512-gmVNCXZiv/CIk2eKRLnH19N9VsPuE2s3Oxm0MNi003zk1cLy7D4YEm4fSrjKXtPY8MMpRXiu5f63W94hLwWEVw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.6.tgz", + "integrity": "sha512-3tX2/Qw+bZ8XzKitviH8jzNGyY0uohhehhBB57OJOCc+yr4ojy/7SYFnun1lSsRnDztdCE461641X4iQLCQ94w==", "dependencies": { "tslib": "^2.3.0" }, @@ -1589,7 +1386,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.2" + "@angular/core": "18.2.6" }, "peerDependenciesMeta": { "@angular/core": { @@ -1598,9 +1395,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.2.tgz", - "integrity": "sha512-fF7lDrTA12YGqVjF4LyMi4hm58cv9G6CWmzSlvun0nMYCwrbRNnakZsj19dOfiIqqu4MwHaF4w3PTmUSxkMuiw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.6.tgz", + "integrity": "sha512-b5x9STfjNiNM/S0D+CnqRP9UOxPtSz1+RlCH5WdOMiW/p8j5p6dBix8YYgTe6Wg3OD7eItD2pnFQKgF/dWiopA==", "dependencies": { "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -1620,14 +1417,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.2", + "@angular/compiler": "18.2.6", "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.2.tgz", - "integrity": "sha512-Rx6XajL0Ydj9hXUSPDvL2Q/kMzWtbiE3VxZFJnkE+fLQiWvr0GncB+NTb/nQ6QlPQ0ly60DvuI3KLcGDuFtGVA==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.6.tgz", + "integrity": "sha512-PjFad2j4YBwLVTw+0Te8CJCa/tV0W8caTHG8aOjj3ObdL6ihGI+FKnwerLc9RVzDFd14BOO4C6/+LbOQAh3Ltw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1640,9 +1437,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.2.tgz", - "integrity": "sha512-K8cv0w6o7+ocQfUrdSA3XaKrYfa1+2TlmtyxPHjEd2mCu2R+Yqo5RqJ3P8keFewJ1+bSLhz6xnn6mumwl0RnUQ==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.6.tgz", + "integrity": "sha512-quGkUqTxlBaLB8C/RnpfFG57fdmNF5RQ+368N89Ma++2lpIsVAHaGZZn4yOyo3wNYaM2jBxNqaYxOzZNUl5Tig==", "dependencies": { "tslib": "^2.3.0" }, @@ -1650,16 +1447,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.2", - "@angular/core": "18.2.2", - "@angular/platform-browser": "18.2.2", + "@angular/common": "18.2.6", + "@angular/core": "18.2.6", + "@angular/platform-browser": "18.2.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.2.tgz", - "integrity": "sha512-grWQ3CVbizOWCthGpyIlNNnZCpF/xpWYa6tIsPzKOXLCyqFQ7vOEtSludNN1nsUmMlZQt76+wA17Fx0qcNx0EA==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.6.tgz", + "integrity": "sha512-4NZwh5EAyXItmwv6hqilV+JyN8DT+d+S1rW+M1IwJqC9asCDfpFqipKpuQF81LQKeLH0mn/phNfVbnJCLP0Tkw==", "dependencies": { "@babel/core": "7.25.2", "@types/babel__core": "7.20.5", @@ -1675,14 +1472,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.2", - "@angular/compiler-cli": "18.2.2" + "@angular/compiler": "18.2.6", + "@angular/compiler-cli": "18.2.6" } }, "node_modules/@angular/platform-browser": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.2.tgz", - "integrity": "sha512-Bfvl8elCFxyJ9vlwamr4X5sVMcp/tSwBal2coyl0WR+/PH2PAAtf+/WMYxIN90yZmPiJx6RZWUSJRlHOFiFp3A==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.6.tgz", + "integrity": "sha512-RA8UMiYNLga+QMwpKcDw1357gYPfPyY/rmLeezMak//BbsENFYQOJ4Z6DBOBNiPlHxmBsUJMGaKdlpQhfCROyQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1690,9 +1487,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.2", - "@angular/common": "18.2.2", - "@angular/core": "18.2.2" + "@angular/animations": "18.2.6", + "@angular/common": "18.2.6", + "@angular/core": "18.2.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1701,9 +1498,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.2.tgz", - "integrity": "sha512-UM/+1nY4iIj1v4lxAmV3XRHPAh/4qfNKScCLq8tJGot64rPCbtCl0Rl8rFFGqxAFvTErVDaJycUgWNZSfVl/hw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.6.tgz", + "integrity": "sha512-kGBU3FNc+DF9r33hwHZqiWoZgQbCDdEIucU0NCLCIg0Hw6/Q9Hr2ndjxQI+WynCPg0JeBn34jpouvpeJer3YDQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1711,16 +1508,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.2", - "@angular/compiler": "18.2.2", - "@angular/core": "18.2.2", - "@angular/platform-browser": "18.2.2" + "@angular/common": "18.2.6", + "@angular/compiler": "18.2.6", + "@angular/core": "18.2.6", + "@angular/platform-browser": "18.2.6" } }, "node_modules/@angular/router": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.2.tgz", - "integrity": "sha512-tBHwuNtZNjzYAoVdveTI1ke/ZnQjKhc7gqDk9HCH2JUpdQhGbTvCKwDM51ktJpPMPcZlA263lQyy7VIyvdtK0A==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.6.tgz", + "integrity": "sha512-t57Sqja8unHhZlPr+4CWnQacuox2M4p2pMHps+31wt337qH6mKf4jqDmK0dE/MFdRyKjT2a2E/2NwtxXxcWNuw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1728,9 +1525,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.2", - "@angular/core": "18.2.2", - "@angular/platform-browser": "18.2.2", + "@angular/common": "18.2.6", + "@angular/core": "18.2.6", + "@angular/platform-browser": "18.2.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -2324,12 +2121,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -5620,9 +5417,9 @@ } }, "node_modules/@ng-select/ng-select": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.7.0.tgz", - "integrity": "sha512-GMNu3bLYxWAbgy9pXZ4RgnWp/cxRcrWRQdxLLyg8p9gMCLpim1p4TXR8laXJKK25MKG/LEaWgs+90yCVOoWgZA==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.9.0.tgz", + "integrity": "sha512-FqoNEvWZSfUWjirpUjsfESpgzZ3sEqSKcIbUirBmviKnYMbyUZbtEChjox+ha2vtHLpdA+/mDoYT5fL8Z/0qJA==", "dependencies": { "tslib": "^2.3.1" }, @@ -5652,9 +5449,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.2.tgz", - "integrity": "sha512-YhADmc+lVjLt3kze07A+yLry2yzcghdclu+7D3EDfa6fG2Pk33HK3MY2I0Z0BO+Ivoq7cV7yxm+naR+Od0Y5ng==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.6.tgz", + "integrity": "sha512-7HwOPE1EOgcHnpt4brSiT8G2CcXB50G0+CbCBaKGy4LYCG3Y3mrlzF5Fup9HvMJ6Tzqd62RqzpKKYBiGUT7hxg==", "dev": true, "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0", @@ -6192,9 +5989,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", - "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.23.0.tgz", + "integrity": "sha512-8OR+Ok3SGEMsAZispLx8jruuXw0HVF16k+ub2eNXKHDmdxL4cf9NlNpAzhlOhNyXzKDEJuFeq0nZm+XlNb1IFw==", "cpu": [ "arm" ], @@ -6205,9 +6002,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", - "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.23.0.tgz", + "integrity": "sha512-rEFtX1nP8gqmLmPZsXRMoLVNB5JBwOzIAk/XAcEPuKrPa2nPJ+DuGGpfQUR0XjRm8KjHfTZLpWbKXkA5BoFL3w==", "cpu": [ "arm64" ], @@ -6218,9 +6015,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", - "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.23.0.tgz", + "integrity": "sha512-ZbqlMkJRMMPeapfaU4drYHns7Q5MIxjM/QeOO62qQZGPh9XWziap+NF9fsqPHT0KzEL6HaPspC7sOwpgyA3J9g==", "cpu": [ "arm64" ], @@ -6231,9 +6028,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", - "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.23.0.tgz", + "integrity": "sha512-PfmgQp78xx5rBCgn2oYPQ1rQTtOaQCna0kRaBlc5w7RlA3TDGGo7m3XaptgitUZ54US9915i7KeVPHoy3/W8tA==", "cpu": [ "x64" ], @@ -6244,9 +6041,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", - "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.23.0.tgz", + "integrity": "sha512-WAeZfAAPus56eQgBioezXRRzArAjWJGjNo/M+BHZygUcs9EePIuGI1Wfc6U/Ki+tMW17FFGvhCfYnfcKPh18SA==", "cpu": [ "arm" ], @@ -6257,9 +6054,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", - "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.23.0.tgz", + "integrity": "sha512-v7PGcp1O5XKZxKX8phTXtmJDVpE20Ub1eF6w9iMmI3qrrPak6yR9/5eeq7ziLMrMTjppkkskXyxnmm00HdtXjA==", "cpu": [ "arm" ], @@ -6270,9 +6067,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", - "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.23.0.tgz", + "integrity": "sha512-nAbWsDZ9UkU6xQiXEyXBNHAKbzSAi95H3gTStJq9UGiS1v+YVXwRHcQOQEF/3CHuhX5BVhShKoeOf6Q/1M+Zhg==", "cpu": [ "arm64" ], @@ -6283,9 +6080,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", - "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.23.0.tgz", + "integrity": "sha512-5QT/Di5FbGNPaVw8hHO1wETunwkPuZBIu6W+5GNArlKHD9fkMHy7vS8zGHJk38oObXfWdsuLMogD4sBySLJ54g==", "cpu": [ "arm64" ], @@ -6296,9 +6093,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", - "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.23.0.tgz", + "integrity": "sha512-Sefl6vPyn5axzCsO13r1sHLcmPuiSOrKIImnq34CBurntcJ+lkQgAaTt/9JkgGmaZJ+OkaHmAJl4Bfd0DmdtOQ==", "cpu": [ "ppc64" ], @@ -6309,9 +6106,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", - "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.23.0.tgz", + "integrity": "sha512-o4QI2KU/QbP7ZExMse6ULotdV3oJUYMrdx3rBZCgUF3ur3gJPfe8Fuasn6tia16c5kZBBw0aTmaUygad6VB/hQ==", "cpu": [ "riscv64" ], @@ -6322,9 +6119,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", - "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.23.0.tgz", + "integrity": "sha512-+bxqx+V/D4FGrpXzPGKp/SEZIZ8cIW3K7wOtcJAoCrmXvzRtmdUhYNbgd+RztLzfDEfA2WtKj5F4tcbNPuqgeg==", "cpu": [ "s390x" ], @@ -6335,9 +6132,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.23.0.tgz", + "integrity": "sha512-I/eXsdVoCKtSgK9OwyQKPAfricWKUMNCwJKtatRYMmDo5N859tbO3UsBw5kT3dU1n6ZcM1JDzPRSGhAUkxfLxw==", "cpu": [ "x64" ], @@ -6348,9 +6145,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.23.0.tgz", + "integrity": "sha512-4ZoDZy5ShLbbe1KPSafbFh1vbl0asTVfkABC7eWqIs01+66ncM82YJxV2VtV3YVJTqq2P8HMx3DCoRSWB/N3rw==", "cpu": [ "x64" ], @@ -6361,9 +6158,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", - "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.23.0.tgz", + "integrity": "sha512-+5Ky8dhft4STaOEbZu3/NU4QIyYssKO+r1cD3FzuusA0vO5gso15on7qGzKdNXnc1gOrsgCqZjRw1w+zL4y4hQ==", "cpu": [ "arm64" ], @@ -6374,9 +6171,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", - "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.23.0.tgz", + "integrity": "sha512-0SPJk4cPZQhq9qA1UhIRumSE3+JJIBBjtlGl5PNC///BoaByckNZd53rOYD0glpTkYFBQSt7AkMeLVPfx65+BQ==", "cpu": [ "ia32" ], @@ -6387,9 +6184,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", - "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz", + "integrity": "sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==", "cpu": [ "x64" ], @@ -6400,13 +6197,13 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.2.tgz", - "integrity": "sha512-0uPA1kQ38RnbNrzMlveX/QAqQIDu2INl5IYd3EUbJZRfYSp1VVyOSyuIBJ+1iUl5Y5VUa2uylaVZXhFdKWprXw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.6.tgz", + "integrity": "sha512-Y988EoOEQDLEyHu3414T6AeVUyx21AexBHQNbUNQkK8cxlxyB6m1eH1cx6vFgLRFUTsLVv+C6Ln/ICNTfLcG4A==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.2", - "@angular-devkit/schematics": "18.2.2", + "@angular-devkit/core": "18.2.6", + "@angular-devkit/schematics": "18.2.6", "jsonc-parser": "3.3.1" }, "engines": { @@ -6680,9 +6477,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/express": { @@ -6698,9 +6495,21 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "dependencies": { "@types/node": "*", @@ -6725,9 +6534,9 @@ "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -6818,9 +6627,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", "dev": true }, "node_modules/@types/range-parser": { @@ -7825,13 +7634,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -8003,21 +7812,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -8837,57 +8631,13 @@ "node": ">=10.13.0" } }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", - "dev": true, - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "dev": true, "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.23.3" }, "funding": { "type": "opencollective", @@ -9385,6 +9135,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -9601,9 +9363,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "engines": { "node": ">= 0.8" @@ -10260,9 +10022,9 @@ "dev": true }, "node_modules/express": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", - "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -10277,7 +10039,7 @@ "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", @@ -10286,11 +10048,11 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", - "serve-static": "1.16.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -10310,15 +10072,6 @@ "ms": "2.0.0" } }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -10436,13 +10189,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10535,9 +10288,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -10800,6 +10553,38 @@ "node": ">=4" } }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -13621,9 +13406,9 @@ } }, "node_modules/launch-editor": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", - "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -13882,6 +13667,12 @@ "@lmdb/lmdb-win32-x64": "3.0.13" } }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -14327,9 +14118,9 @@ } }, "node_modules/memfs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz", - "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.12.0.tgz", + "integrity": "sha512-74wDsex5tQDSClVkeK1vtxqYCAgCoXxx+K4NSHzgU/muYVYByFqa+0RnrPO9NM6naWm1+G9JmZ0p6QHhXmeYfA==", "dev": true, "dependencies": { "@jsonjoy.com/json-pack": "^1.0.3", @@ -14841,9 +14632,9 @@ "dev": true }, "node_modules/ng2-pdf-viewer": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.3.0.tgz", - "integrity": "sha512-zU51lVcsmCy1Nytw94r2ABHfdBKlJWc+Zllk7Fct3pT3b7Q8UbMiZ8IbA4d5iXoe2/iznsS2YXGzMn0/vPHcXA==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.3.1.tgz", + "integrity": "sha512-Tdeu74Go1qzBMmpVbiKA96bgtHCeh+Qnq7ErVLHsV/9TPoCO6cRBsGW/7ojdH1S92MnnM6/iQGLY3EnMYh1wOg==", "dependencies": { "pdfjs-dist": "^4.5.136", "tslib": "^2.3.0" @@ -14950,19 +14741,13 @@ "node-gyp-build": "^4.2.2" } }, - "node_modules/nice-napi/node_modules/node-addon-api": { + "node_modules/node-addon-api": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true, "optional": true }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -15418,6 +15203,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -15541,9 +15359,9 @@ } }, "node_modules/ordered-binary": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", - "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.2.tgz", + "integrity": "sha512-JTo+4+4Fw7FreyAvlSLjb1BBVaxEQAacmjD3jjuyPZclpbEghTvQZbXBb2qPd2LeIMxiHwXBZUcpmG2Gl/mDEA==", "dev": true }, "node_modules/os-tmpdir": { @@ -15648,9 +15466,9 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, "node_modules/pacote": { @@ -15992,6 +15810,18 @@ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path2d": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", @@ -16014,9 +15844,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "4.0.2", @@ -16144,9 +15974,9 @@ } }, "node_modules/pkg-dir/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, "engines": { "node": ">=12.20" @@ -16324,9 +16154,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -16485,12 +16315,12 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -16620,9 +16450,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -16632,9 +16462,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, "node_modules/regenerator-transform": { @@ -16647,9 +16477,9 @@ } }, "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true }, "node_modules/regexpu-core": { @@ -16852,12 +16682,12 @@ } }, "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.23.0.tgz", + "integrity": "sha512-vXB4IT9/KLDrS2WRXmY22sVB2wTsTwkpxjB8Q3mnakTENcYw3FRmfdYDy/acNmls+lHmDazgrRjK/yQ6hQAtwA==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -16867,22 +16697,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", + "@rollup/rollup-android-arm-eabi": "4.23.0", + "@rollup/rollup-android-arm64": "4.23.0", + "@rollup/rollup-darwin-arm64": "4.23.0", + "@rollup/rollup-darwin-x64": "4.23.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.23.0", + "@rollup/rollup-linux-arm-musleabihf": "4.23.0", + "@rollup/rollup-linux-arm64-gnu": "4.23.0", + "@rollup/rollup-linux-arm64-musl": "4.23.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.23.0", + "@rollup/rollup-linux-riscv64-gnu": "4.23.0", + "@rollup/rollup-linux-s390x-gnu": "4.23.0", + "@rollup/rollup-linux-x64-gnu": "4.23.0", + "@rollup/rollup-linux-x64-musl": "4.23.0", + "@rollup/rollup-win32-arm64-msvc": "4.23.0", + "@rollup/rollup-win32-ia32-msvc": "4.23.0", + "@rollup/rollup-win32-x64-msvc": "4.23.0", "fsevents": "~2.3.2" } }, @@ -17012,9 +16842,9 @@ } }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, "optional": true }, @@ -17118,6 +16948,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -17212,60 +17051,15 @@ } }, "node_modules/serve-static": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", - "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/serve-static/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -17537,9 +17331,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -18435,9 +18229,9 @@ "dev": true }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "engines": { "node": ">=4" @@ -18457,9 +18251,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "engines": { "node": ">=4" @@ -18687,14 +18481,14 @@ } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -18745,6 +18539,34 @@ } } }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -18859,9 +18681,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz", - "integrity": "sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -18955,18 +18777,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-dev-server/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -19011,21 +18821,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-dev-server/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -19041,24 +18836,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/webpack-dev-server/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "dev": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", diff --git a/src-ui/package.json b/src-ui/package.json index ef0bf17bc..a12b21b5d 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -11,23 +11,23 @@ }, "private": true, "dependencies": { - "@angular/cdk": "^18.2.2", - "@angular/common": "~18.2.2", - "@angular/compiler": "~18.2.2", - "@angular/core": "~18.2.2", - "@angular/forms": "~18.2.2", - "@angular/localize": "~18.2.2", - "@angular/platform-browser": "~18.2.2", - "@angular/platform-browser-dynamic": "~18.2.2", - "@angular/router": "~18.2.2", + "@angular/cdk": "^18.2.6", + "@angular/common": "~18.2.6", + "@angular/compiler": "~18.2.6", + "@angular/core": "~18.2.6", + "@angular/forms": "~18.2.6", + "@angular/localize": "~18.2.6", + "@angular/platform-browser": "~18.2.6", + "@angular/platform-browser-dynamic": "~18.2.6", + "@angular/router": "~18.2.6", "@ng-bootstrap/ng-bootstrap": "^17.0.1", - "@ng-select/ng-select": "^13.7.0", + "@ng-select/ng-select": "^13.9.0", "@ngneat/dirty-check-forms": "^3.0.3", "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "file-saver": "^2.0.5", "mime-names": "^1.0.0", - "ng2-pdf-viewer": "^10.3.0", + "ng2-pdf-viewer": "^10.3.1", "ngx-bootstrap-icons": "^1.9.3", "ngx-color": "^9.0.0", "ngx-cookie-service": "^18.0.0", @@ -42,14 +42,14 @@ "@angular-builders/custom-webpack": "^18.0.0", "@angular-builders/jest": "^18.0.0", "@angular-devkit/build-angular": "^18.2.2", - "@angular-devkit/core": "^18.2.2", - "@angular-devkit/schematics": "^18.2.2", - "@angular-eslint/builder": "18.3.0", - "@angular-eslint/eslint-plugin": "18.3.0", - "@angular-eslint/eslint-plugin-template": "18.3.0", - "@angular-eslint/schematics": "18.3.0", - "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "~18.2.2", + "@angular-devkit/core": "^18.2.6", + "@angular-devkit/schematics": "^18.2.6", + "@angular-eslint/builder": "18.3.1", + "@angular-eslint/eslint-plugin": "18.3.1", + "@angular-eslint/eslint-plugin-template": "18.3.1", + "@angular-eslint/schematics": "18.3.1", + "@angular-eslint/template-parser": "18.3.1", + "@angular/cli": "~18.2.6", "@angular/compiler-cli": "~18.2.2", "@codecov/webpack-plugin": "^1.0.1", "@playwright/test": "^1.46.1", From 38974a0ee933a7f58acb8320131b59ecf0a747de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:59:11 -0700 Subject: [PATCH 27/74] Chore(deps-dev): Bump @playwright/test from 1.46.1 to 1.47.2 in /src-ui (#7828) Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.46.1 to 1.47.2. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.46.1...v1.47.2) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src-ui/package-lock.json | 24 ++++++++++++------------ src-ui/package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 7871bd71e..862748b7f 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -50,7 +50,7 @@ "@angular/cli": "~18.2.6", "@angular/compiler-cli": "~18.2.2", "@codecov/webpack-plugin": "^1.0.1", - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.47.2", "@types/jest": "^29.5.12", "@types/node": "^22.0.2", "@typescript-eslint/eslint-plugin": "^8.3.0", @@ -5965,12 +5965,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", - "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz", + "integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==", "dev": true, "dependencies": { - "playwright": "1.46.1" + "playwright": "1.47.2" }, "bin": { "playwright": "cli.js" @@ -15986,12 +15986,12 @@ } }, "node_modules/playwright": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", - "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", + "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==", "dev": true, "dependencies": { - "playwright-core": "1.46.1" + "playwright-core": "1.47.2" }, "bin": { "playwright": "cli.js" @@ -16004,9 +16004,9 @@ } }, "node_modules/playwright-core": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", - "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz", + "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==", "dev": true, "bin": { "playwright-core": "cli.js" diff --git a/src-ui/package.json b/src-ui/package.json index a12b21b5d..be2e37cc8 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -52,7 +52,7 @@ "@angular/cli": "~18.2.6", "@angular/compiler-cli": "~18.2.2", "@codecov/webpack-plugin": "^1.0.1", - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.47.2", "@types/jest": "^29.5.12", "@types/node": "^22.0.2", "@typescript-eslint/eslint-plugin": "^8.3.0", From d11dfd4053c37867d75e476f9e5668e9999f2370 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:10:16 +0000 Subject: [PATCH 28/74] Chore(deps-dev): Bump the frontend-jest-dependencies group (#7826) Bumps the frontend-jest-dependencies group in /src-ui with 2 updates: [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) and [jest-preset-angular](https://github.com/thymikee/jest-preset-angular). Updates `@types/jest` from 29.5.12 to 29.5.13 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest) Updates `jest-preset-angular` from 14.2.2 to 14.2.4 - [Release notes](https://github.com/thymikee/jest-preset-angular/releases) - [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/thymikee/jest-preset-angular/compare/v14.2.2...v14.2.4) --- updated-dependencies: - dependency-name: "@types/jest" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-jest-dependencies - dependency-name: jest-preset-angular dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-jest-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src-ui/package-lock.json | 16 ++++++++-------- src-ui/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 862748b7f..96843936c 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -51,7 +51,7 @@ "@angular/compiler-cli": "~18.2.2", "@codecov/webpack-plugin": "^1.0.1", "@playwright/test": "^1.47.2", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/node": "^22.0.2", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", @@ -59,7 +59,7 @@ "eslint": "^9.9.1", "jest": "29.7.0", "jest-environment-jsdom": "^29.7.0", - "jest-preset-angular": "^14.2.2", + "jest-preset-angular": "^14.2.4", "jest-websocket-mock": "^2.5.0", "patch-package": "^8.0.0", "ts-node": "~10.9.1", @@ -6567,9 +6567,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "29.5.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -12357,9 +12357,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.2.2", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.2.2.tgz", - "integrity": "sha512-vdpv1DV4yJMMoBRbTdwRA16Es0UU+8CuOHsV2vfUL0LOy69anvq2RUawh07EyTWSVxko838jOC146jlnCkWOOw==", + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.2.4.tgz", + "integrity": "sha512-xyhkaiBdn3keBgxxkcbqZu/my3ADU9NcDrz6DaMuGRaxz/bf6ZC1qxZ1eQuz5V1WuA3/rD64VA3Kke8P6E9qNg==", "dev": true, "dependencies": { "bs-logger": "^0.2.6", diff --git a/src-ui/package.json b/src-ui/package.json index be2e37cc8..5470daa04 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -53,7 +53,7 @@ "@angular/compiler-cli": "~18.2.2", "@codecov/webpack-plugin": "^1.0.1", "@playwright/test": "^1.47.2", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/node": "^22.0.2", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", @@ -61,7 +61,7 @@ "eslint": "^9.9.1", "jest": "29.7.0", "jest-environment-jsdom": "^29.7.0", - "jest-preset-angular": "^14.2.2", + "jest-preset-angular": "^14.2.4", "jest-websocket-mock": "^2.5.0", "patch-package": "^8.0.0", "ts-node": "~10.9.1", From d242d0895c5938049d9b55e089d966c76d72bd5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:20:53 +0000 Subject: [PATCH 29/74] Chore(deps-dev): Bump the frontend-eslint-dependencies group (#7827) Bumps the frontend-eslint-dependencies group in /src-ui with 4 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser), [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) and [eslint](https://github.com/eslint/eslint). Updates `@typescript-eslint/eslint-plugin` from 8.3.0 to 8.8.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.8.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.3.0 to 8.8.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.8.0/packages/parser) Updates `@typescript-eslint/utils` from 8.3.0 to 8.8.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.8.0/packages/utils) Updates `eslint` from 9.9.1 to 9.11.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.9.1...v9.11.1) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: frontend-eslint-dependencies - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: frontend-eslint-dependencies - dependency-name: "@typescript-eslint/utils" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: frontend-eslint-dependencies - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor dependency-group: frontend-eslint-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src-ui/package-lock.json | 130 +++++++++++++++++++++++---------------- src-ui/package.json | 6 +- 2 files changed, 80 insertions(+), 56 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 96843936c..686f338ab 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -53,10 +53,10 @@ "@playwright/test": "^1.47.2", "@types/jest": "^29.5.13", "@types/node": "^22.0.2", - "@typescript-eslint/eslint-plugin": "^8.3.0", - "@typescript-eslint/parser": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/parser": "^8.8.0", "@typescript-eslint/utils": "^8.0.0", - "eslint": "^9.9.1", + "eslint": "^9.11.1", "jest": "29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-preset-angular": "^14.2.4", @@ -3894,6 +3894,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", @@ -3970,9 +3979,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", - "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", + "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3987,6 +3996,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -6726,16 +6747,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", - "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", + "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/type-utils": "8.3.0", - "@typescript-eslint/utils": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/type-utils": "8.8.0", + "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6759,15 +6780,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", - "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", + "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", "debug": "^4.3.4" }, "engines": { @@ -6787,13 +6808,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", - "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", + "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0" + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6804,13 +6825,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", - "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", + "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/utils": "8.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -6828,9 +6849,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", - "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", + "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6841,13 +6862,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", - "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", + "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6893,15 +6914,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", - "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", + "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0" + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6915,12 +6936,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", - "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", + "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/types": "8.8.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -9599,19 +9620,23 @@ } }, "node_modules/eslint": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", - "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", + "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.9.1", + "@eslint/js": "9.11.1", + "@eslint/plugin-kit": "^0.2.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -9631,7 +9656,6 @@ "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", diff --git a/src-ui/package.json b/src-ui/package.json index 5470daa04..ce81fd266 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -55,10 +55,10 @@ "@playwright/test": "^1.47.2", "@types/jest": "^29.5.13", "@types/node": "^22.0.2", - "@typescript-eslint/eslint-plugin": "^8.3.0", - "@typescript-eslint/parser": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/parser": "^8.8.0", "@typescript-eslint/utils": "^8.0.0", - "eslint": "^9.9.1", + "eslint": "^9.11.1", "jest": "29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-preset-angular": "^14.2.4", From 09deda73bd0312ec2dc656c5c95087a1061f63eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:29:28 +0000 Subject: [PATCH 30/74] Chore(deps-dev): Bump @types/node from 22.5.2 to 22.7.4 in /src-ui (#7829) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.2 to 22.7.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src-ui/package-lock.json | 8 ++++---- src-ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 686f338ab..74806061a 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -52,7 +52,7 @@ "@codecov/webpack-plugin": "^1.0.1", "@playwright/test": "^1.47.2", "@types/jest": "^29.5.13", - "@types/node": "^22.0.2", + "@types/node": "^22.7.4", "@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/parser": "^8.8.0", "@typescript-eslint/utils": "^8.0.0", @@ -6630,9 +6630,9 @@ } }, "node_modules/@types/node": { - "version": "22.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz", - "integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==", + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "dev": true, "dependencies": { "undici-types": "~6.19.2" diff --git a/src-ui/package.json b/src-ui/package.json index ce81fd266..488afd273 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -54,7 +54,7 @@ "@codecov/webpack-plugin": "^1.0.1", "@playwright/test": "^1.47.2", "@types/jest": "^29.5.13", - "@types/node": "^22.0.2", + "@types/node": "^22.7.4", "@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/parser": "^8.8.0", "@typescript-eslint/utils": "^8.0.0", From feb9a62b249b83d2d9212d2ae701c03ddc772dc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:38:03 +0000 Subject: [PATCH 31/74] Chore(deps-dev): Bump @codecov/webpack-plugin in /src-ui (#7830) Bumps @codecov/webpack-plugin from 1.0.1 to 1.2.0. --- updated-dependencies: - dependency-name: "@codecov/webpack-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src-ui/package-lock.json | 79 +++++++++++----------------------------- src-ui/package.json | 2 +- 2 files changed, 23 insertions(+), 58 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 74806061a..4aa06ddf0 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -49,7 +49,7 @@ "@angular-eslint/template-parser": "18.3.1", "@angular/cli": "~18.2.6", "@angular/compiler-cli": "~18.2.2", - "@codecov/webpack-plugin": "^1.0.1", + "@codecov/webpack-plugin": "^1.2.0", "@playwright/test": "^1.47.2", "@types/jest": "^29.5.13", "@types/node": "^22.7.4", @@ -80,7 +80,6 @@ "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", "dev": true, - "license": "MIT", "dependencies": { "@actions/http-client": "^2.0.1", "uuid": "^8.3.2" @@ -91,7 +90,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, - "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -101,7 +99,6 @@ "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", "dev": true, - "license": "MIT", "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", @@ -114,7 +111,6 @@ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", "dev": true, - "license": "MIT", "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" @@ -3323,11 +3319,10 @@ "dev": true }, "node_modules/@codecov/bundler-plugin-core": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.0.1.tgz", - "integrity": "sha512-Uo150Qb2s/mMqqfZMdh6rC1+Cp+bCij5DAB6LqWNI6J9dGbimeNvpU1+jdQ6vlMJOiz5w5jAOhtgZvFNrc8jUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.2.0.tgz", + "integrity": "sha512-ublUP5V0tW6oDnaJ1UBWvEmVAkvMmPNEwWkpF+WwJSCBWNLvWrkSwG84S3Gt5Xbnh17xEyAxXBmNzF+mXVXBgw==", "dev": true, - "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", @@ -3345,7 +3340,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -3361,7 +3355,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3378,7 +3371,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -3390,15 +3382,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@codecov/bundler-plugin-core/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -3408,7 +3398,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3417,13 +3406,12 @@ } }, "node_modules/@codecov/webpack-plugin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@codecov/webpack-plugin/-/webpack-plugin-1.0.1.tgz", - "integrity": "sha512-e6VpcP3adF5ig2OXjb/mrdZ4o8gluKc/IvTAAZfhjX4CWIsnuyRQqFobKyC9nKMRWpIGCsuxdmamyQSrfwXIUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@codecov/webpack-plugin/-/webpack-plugin-1.2.0.tgz", + "integrity": "sha512-yawRyKgC8tXj/C/UoTJ+kRMePfhkYJtJey+xmywRqmVmvbxQGnYFkg+Dzix/HxKt4iGpSwKT4p8Glz2MNQ7gTQ==", "dev": true, - "license": "MIT", "dependencies": { - "@codecov/bundler-plugin-core": "^1.0.1", + "@codecov/bundler-plugin-core": "^1.2.0", "unplugin": "^1.10.1" }, "engines": { @@ -4013,7 +4001,6 @@ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" } @@ -5808,7 +5795,6 @@ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 18" } @@ -5818,7 +5804,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -5837,7 +5822,6 @@ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" @@ -5851,7 +5835,6 @@ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/request": "^8.3.0", "@octokit/types": "^13.0.0", @@ -5865,15 +5848,13 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/types": "^12.6.0" }, @@ -5888,15 +5869,13 @@ "version": "20.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/openapi-types": "^20.0.0" } @@ -5906,7 +5885,6 @@ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/types": "^12.6.0" }, @@ -5921,15 +5899,13 @@ "version": "20.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/openapi-types": "^20.0.0" } @@ -5939,7 +5915,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/endpoint": "^9.0.1", "@octokit/request-error": "^5.1.0", @@ -5955,7 +5930,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", @@ -5966,11 +5940,10 @@ } }, "node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.0.tgz", + "integrity": "sha512-CrooV/vKCXqwLa+osmHLIMUb87brpgUqlqkPGc6iE2wCkUvTrHiXFMhAKoDDaAAYJrtKtrFTgSQTg5nObBEaew==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/openapi-types": "^22.2.0" } @@ -7754,8 +7727,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true, - "license": "Apache-2.0" + "dev": true }, "node_modules/big.js": { "version": "5.2.2", @@ -9196,8 +9168,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/dequal": { "version": "2.0.3", @@ -18164,7 +18135,6 @@ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -18238,7 +18208,6 @@ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", "dev": true, - "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -18332,8 +18301,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/universalify": { "version": "2.0.1", @@ -18354,11 +18322,10 @@ } }, "node_modules/unplugin": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.0.tgz", - "integrity": "sha512-cfkZeALGyW7tKYjZbi0G+pn0XnUFa0QvLIeLJEUUlnU0R8YYsBQnt5+h9Eu1B7AB7KETld+UBFI5lOeBL+msoQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.1.tgz", + "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", "dev": true, - "license": "MIT", "dependencies": { "acorn": "^8.12.1", "webpack-virtual-modules": "^0.6.2" @@ -18923,8 +18890,7 @@ "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", @@ -19335,7 +19301,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src-ui/package.json b/src-ui/package.json index 488afd273..f46f06f64 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -51,7 +51,7 @@ "@angular-eslint/template-parser": "18.3.1", "@angular/cli": "~18.2.6", "@angular/compiler-cli": "~18.2.2", - "@codecov/webpack-plugin": "^1.0.1", + "@codecov/webpack-plugin": "^1.2.0", "@playwright/test": "^1.47.2", "@types/jest": "^29.5.13", "@types/node": "^22.7.4", From 81df9c79fde61a0346f4b7e5efe6d126346d981d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:13:45 -0700 Subject: [PATCH 32/74] Fix: wrap table header columns in row (#7832) --- src-ui/messages.xlf | 58 ++--- .../document-list.component.html | 202 +++++++++--------- .../document-list.component.spec.ts | 2 +- 3 files changed, 132 insertions(+), 130 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index f5d270376..9b588ac6b 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -1042,7 +1042,7 @@ src/app/components/document-list/document-list.component.html - 211 + 212 src/app/data/document.ts @@ -1950,7 +1950,7 @@ src/app/components/document-list/document-list.component.html - 238 + 239 src/app/data/document.ts @@ -2752,7 +2752,7 @@ src/app/components/document-list/document-list.component.html - 193 + 194 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -3341,7 +3341,7 @@ src/app/components/document-list/document-list.component.html - 247 + 248 src/app/data/document.ts @@ -5510,7 +5510,7 @@ src/app/components/document-list/document-list.component.html - 286 + 288 @@ -5525,7 +5525,7 @@ src/app/components/document-list/document-list.component.html - 321 + 323 @@ -5540,7 +5540,7 @@ src/app/components/document-list/document-list.component.html - 328 + 330 @@ -5830,7 +5830,7 @@ src/app/components/document-list/document-list.component.html - 190 + 191 src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -5871,7 +5871,7 @@ src/app/components/document-list/document-list.component.html - 180 + 181 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -5898,7 +5898,7 @@ src/app/components/document-list/document-list.component.html - 220 + 221 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -5925,7 +5925,7 @@ src/app/components/document-list/document-list.component.html - 229 + 230 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -6726,7 +6726,7 @@ src/app/components/document-list/document-list.component.html - 297 + 299 @@ -6939,14 +6939,14 @@ Sort by ASN src/app/components/document-list/document-list.component.html - 167 + 168 ASN src/app/components/document-list/document-list.component.html - 171 + 172 src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -6965,28 +6965,28 @@ Sort by correspondent src/app/components/document-list/document-list.component.html - 176 + 177 Sort by title src/app/components/document-list/document-list.component.html - 185 + 186 Sort by owner src/app/components/document-list/document-list.component.html - 198 + 199 Owner src/app/components/document-list/document-list.component.html - 202 + 203 src/app/data/document.ts @@ -7001,49 +7001,49 @@ Sort by notes src/app/components/document-list/document-list.component.html - 207 + 208 Sort by document type src/app/components/document-list/document-list.component.html - 216 + 217 Sort by storage path src/app/components/document-list/document-list.component.html - 225 + 226 Sort by created date src/app/components/document-list/document-list.component.html - 234 + 235 Sort by added date src/app/components/document-list/document-list.component.html - 243 + 244 Sort by number of pages src/app/components/document-list/document-list.component.html - 252 + 253 Pages src/app/components/document-list/document-list.component.html - 256 + 257 src/app/data/document.ts @@ -7062,21 +7062,21 @@ Shared src/app/components/document-list/document-list.component.html - 259,261 + 260,262 Edit document src/app/components/document-list/document-list.component.html - 293 + 295 Yes src/app/components/document-list/document-list.component.html - 349 + 351 src/app/pipes/yes-no.pipe.ts @@ -7087,7 +7087,7 @@ No src/app/components/document-list/document-list.component.html - 349 + 351 src/app/pipes/yes-no.pipe.ts diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 8ca8e111d..e70f4c710 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -160,111 +160,113 @@
    - - @if (activeDisplayFields.includes(DisplayField.ASN)) { - - } - @if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { - - } - @if (activeDisplayFields.includes(DisplayField.TITLE)) { - - } - @if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) { - - } - @if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) { - - } - @if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) { - - } - @if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { - - } - @if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { - - } - @if (activeDisplayFields.includes(DisplayField.CREATED)) { - - } - @if (activeDisplayFields.includes(DisplayField.ADDED)) { - - } - @if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) { + + + @if (activeDisplayFields.includes(DisplayField.ASN)) { + i18n>ASN } - @if (activeDisplayFields.includes(DisplayField.SHARED)) { - - } - @for (field of activeDisplayCustomFields; track field) { - - } + @if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { + + } + @if (activeDisplayFields.includes(DisplayField.TITLE)) { + + } + @if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) { + + } + @if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) { + + } + @if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) { + + } + @if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { + + } + @if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { + + } + @if (activeDisplayFields.includes(DisplayField.CREATED)) { + + } + @if (activeDisplayFields.includes(DisplayField.ADDED)) { + + } + @if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) { + + } + @if (activeDisplayFields.includes(DisplayField.SHARED)) { + + } + @for (field of activeDisplayCustomFields; track field) { + + } + @for (d of list.documents; track trackByDocumentId($index, d)) { diff --git a/src-ui/src/app/components/document-list/document-list.component.spec.ts b/src-ui/src/app/components/document-list/document-list.component.spec.ts index ad85652b8..0a8faa4d3 100644 --- a/src-ui/src/app/components/document-list/document-list.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-list.component.spec.ts @@ -302,7 +302,7 @@ describe('DocumentListComponent', () => { displayModeButtons[0].triggerEventHandler('change') fixture.detectChanges() expect(component.list.displayMode).toEqual('table') - expect(fixture.debugElement.queryAll(By.css('tr'))).toHaveLength(3) + expect(fixture.debugElement.queryAll(By.css('tr'))).toHaveLength(4) displayModeButtons[1].nativeElement.checked = true displayModeButtons[1].triggerEventHandler('change') From ab0ef94a8efc6f69508980267bab0aff0c6d3fff Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:37:35 -0700 Subject: [PATCH 33/74] Update .codecov.yml --- .codecov.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 331e3a283..5fa8e1639 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -14,8 +14,9 @@ flag_management: # codecov will only comment if coverage changes comment: require_changes: true + # https://docs.codecov.com/docs/javascript-bundle-analysis require_bundle_changes: true - bundle_change_threshold: "1Kb" + bundle_change_threshold: "50Kb" coverage: status: project: @@ -24,7 +25,12 @@ coverage: threshold: 1% patch: default: - # For the changed lines only, target 75% covered, but - # allow as low as 50% - target: 75% + # For the changed lines only, target 100% covered, but + # allow as low as 75% + target: 100% threshold: 25% +# https://docs.codecov.com/docs/javascript-bundle-analysis +bundle_analysis: + # Fail if the bundle size increases by more than 1MB + warning_threshold: "1MB" + status: true From b2e5406aae41ec5413a876ab234b4e98d51309b3 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:15:42 -0700 Subject: [PATCH 34/74] Feature: custom fields queries (#7761) --- docs/api.md | 32 +- paperless.conf.example | 1 - src-ui/messages.xlf | 289 ++++++++++++---- src-ui/src/app/app.module.ts | 8 + ...ustom-fields-query-dropdown.component.html | 163 +++++++++ ...ustom-fields-query-dropdown.component.scss | 43 +++ ...om-fields-query-dropdown.component.spec.ts | 320 ++++++++++++++++++ .../custom-fields-query-dropdown.component.ts | 294 ++++++++++++++++ .../document-link.component.html | 101 +++--- .../document-link/document-link.component.ts | 6 + .../document-list.component.html | 6 +- .../document-list/document-list.component.ts | 4 - .../filter-editor.component.html | 11 +- .../filter-editor.component.spec.ts | 230 +++++-------- .../filter-editor/filter-editor.component.ts | 123 +++---- src-ui/src/app/data/custom-field-query.ts | 127 +++++++ src-ui/src/app/data/filter-rule-type.ts | 8 + .../utils/custom-field-query-element.spec.ts | 245 ++++++++++++++ .../app/utils/custom-field-query-element.ts | 210 ++++++++++++ src-ui/src/app/utils/query-params.spec.ts | 60 +++- src-ui/src/app/utils/query-params.ts | 59 +++- src/documents/filters.py | 88 ++--- ...instance_value_monetary_amount_and_more.py | 95 ++++++ src/documents/models.py | 25 ++ .../tests/test_api_filter_by_custom_fields.py | 164 ++------- src/paperless/settings.py | 17 - 26 files changed, 2130 insertions(+), 599 deletions(-) create mode 100644 src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html create mode 100644 src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss create mode 100644 src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts create mode 100644 src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts create mode 100644 src-ui/src/app/data/custom-field-query.ts create mode 100644 src-ui/src/app/utils/custom-field-query-element.spec.ts create mode 100644 src-ui/src/app/utils/custom-field-query-element.ts create mode 100644 src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py diff --git a/docs/api.md b/docs/api.md index bf9e88659..e5da43a5c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -278,39 +278,39 @@ attribute with various information about the search results: ### Filtering by custom fields You can filter documents by their custom field values by specifying the -`custom_field_lookup` query parameter. Here are some recipes for common +`custom_field_query` query parameter. Here are some recipes for common use cases: 1. Documents with a custom field "due" (date) between Aug 1, 2024 and Sept 1, 2024 (inclusive): - `?custom_field_lookup=["due", "range", ["2024-08-01", "2024-09-01"]]` + `?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]` 2. Documents with a custom field "customer" (text) that equals "bob" (case sensitive): - `?custom_field_lookup=["customer", "exact", "bob"]` + `?custom_field_query=["customer", "exact", "bob"]` 3. Documents with a custom field "answered" (boolean) set to `true`: - `?custom_field_lookup=["answered", "exact", true]` + `?custom_field_query=["answered", "exact", true]` 4. Documents with a custom field "favorite animal" (select) set to either "cat" or "dog": - `?custom_field_lookup=["favorite animal", "in", ["cat", "dog"]]` + `?custom_field_query=["favorite animal", "in", ["cat", "dog"]]` 5. Documents with a custom field "address" (text) that is empty: - `?custom_field_lookup=["OR", ["address", "isnull", true], ["address", "exact", ""]]` + `?custom_field_query=["OR", ["address", "isnull", true], ["address", "exact", ""]]` 6. Documents that don't have a field called "foo": - `?custom_field_lookup=["foo", "exists", false]` + `?custom_field_query=["foo", "exists", false]` 7. Documents that have document links "references" to both document 3 and 7: - `?custom_field_lookup=["references", "contains", [3, 7]]` + `?custom_field_query=["references", "contains", [3, 7]]` All field types support basic operations including `exact`, `in`, `isnull`, and `exists`. String, URL, and monetary fields support case-insensitive @@ -320,22 +320,6 @@ including `gt` (>), `gte` (>=), `lt` (<), `lte` (<=), and `range`. Lastly, document link fields support a `contains` operator that behaves like a "is superset of" check. -!!! warning - - It is possible to do case-insensitive exact match (i.e., `iexact`) and - case-sensitive substring match (i.e., `contains`, `startswith`, - `endswith`) for string, URL, and monetary fields, but - [they may not work as expected on some database backends](https://docs.djangoproject.com/en/5.1/ref/databases/#substring-matching-and-case-sensitivity). - - It is also possible to use regular expressions to match string, URL, and - monetary fields, but the syntax is database-dependent, and accepting - regular expressions from untrusted sources could make your instance - vulnerable to regular expression denial of service attacks. - - For these reasons the above expressions are disabled by default. - If you understand the implications, you may enable them by uncommenting - `PAPERLESS_CUSTOM_FIELD_LOOKUP_OPT_IN` in your configuration file. - ### `/api/search/autocomplete/` Get auto completions for a partial search term. diff --git a/paperless.conf.example b/paperless.conf.example index 5fabbf390..63ee7be22 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -81,7 +81,6 @@ #PAPERLESS_THUMBNAIL_FONT_NAME= #PAPERLESS_IGNORE_DATES= #PAPERLESS_ENABLE_UPDATE_CHECK= -#PAPERLESS_ALLOW_CUSTOM_FIELD_LOOKUP=iexact,contains,startswith,endswith,regex,iregex # Tika settings diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 9b588ac6b..3fffe4f6e 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -698,7 +698,7 @@ src/app/components/common/input/document-link/document-link.component.html - 38 + 51 src/app/components/common/permissions-dialog/permissions-dialog.component.html @@ -1031,7 +1031,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 143 + 152 @@ -1088,7 +1088,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 110 + 105 src/app/components/manage/mail/mail.component.html @@ -3300,6 +3300,102 @@ 63 + + True + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 40 + + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 73 + + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 79 + + + + False + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 41 + + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 74 + + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 80 + + + + Search docs... + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 96 + + + + Any + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 126 + + + src/app/components/common/filterable-dropdown/filterable-dropdown.component.html + 17 + + + + All + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 128 + + + src/app/components/common/filterable-dropdown/filterable-dropdown.component.html + 15 + + + src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html + 16 + + + src/app/components/common/permissions-select/permissions-select.component.html + 16 + + + src/app/components/common/permissions-select/permissions-select.component.html + 27 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 14 + + + + Not + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 131 + + + + Add query + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 150 + + + + Add expression + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 153 + + now @@ -4549,36 +4645,6 @@ 146 - - All - - src/app/components/common/filterable-dropdown/filterable-dropdown.component.html - 15 - - - src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html - 16 - - - src/app/components/common/permissions-select/permissions-select.component.html - 16 - - - src/app/components/common/permissions-select/permissions-select.component.html - 27 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 14 - - - - Any - - src/app/components/common/filterable-dropdown/filterable-dropdown.component.html - 17 - - Include @@ -4668,7 +4734,7 @@ src/app/components/common/input/document-link/document-link.component.html - 9 + 12 src/app/components/common/input/file/file.component.html @@ -4740,14 +4806,14 @@ Remove link src/app/components/common/input/document-link/document-link.component.html - 30 + 43 Open link src/app/components/common/input/document-link/document-link.component.html - 31 + 44 src/app/components/common/input/url/url.component.html @@ -4761,6 +4827,13 @@ 44 + + Search for documents + + src/app/components/common/input/document-link/document-link.component.ts + 53 + + Selected items @@ -5834,7 +5907,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 131 + 140 src/app/data/document.ts @@ -6416,7 +6489,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 139 + 148 @@ -6425,10 +6498,6 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html 83 - - src/app/components/document-list/filter-editor/filter-editor.component.html - 90 - Merge @@ -6925,7 +6994,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 116 + 111 @@ -6950,7 +7019,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 136 + 145 src/app/data/document.ts @@ -7126,161 +7195,154 @@ Dates src/app/components/document-list/filter-editor/filter-editor.component.html - 100 + 95 Title & content src/app/components/document-list/filter-editor/filter-editor.component.ts - 134 + 143 More like src/app/components/document-list/filter-editor/filter-editor.component.ts - 149 + 158 equals src/app/components/document-list/filter-editor/filter-editor.component.ts - 155 + 164 is empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 159 + 168 is not empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 163 + 172 greater than src/app/components/document-list/filter-editor/filter-editor.component.ts - 167 + 176 less than src/app/components/document-list/filter-editor/filter-editor.component.ts - 171 + 180 Correspondent: src/app/components/document-list/filter-editor/filter-editor.component.ts - 191,193 + 200,202 Without correspondent src/app/components/document-list/filter-editor/filter-editor.component.ts - 195 + 204 Document type: src/app/components/document-list/filter-editor/filter-editor.component.ts - 201,203 + 210,212 Without document type src/app/components/document-list/filter-editor/filter-editor.component.ts - 205 + 214 Storage path: src/app/components/document-list/filter-editor/filter-editor.component.ts - 211,213 + 220,222 Without storage path src/app/components/document-list/filter-editor/filter-editor.component.ts - 215 + 224 Tag: src/app/components/document-list/filter-editor/filter-editor.component.ts - 219,221 + 228,230 Without any tag src/app/components/document-list/filter-editor/filter-editor.component.ts - 225 + 234 - - Custom fields: + + Custom fields query src/app/components/document-list/filter-editor/filter-editor.component.ts - 229,231 - - - - Without any custom field - - src/app/components/document-list/filter-editor/filter-editor.component.ts - 235 + 238 Title: src/app/components/document-list/filter-editor/filter-editor.component.ts - 239 + 241 ASN: src/app/components/document-list/filter-editor/filter-editor.component.ts - 242 + 244 Owner: src/app/components/document-list/filter-editor/filter-editor.component.ts - 245 + 247 Owner not in: src/app/components/document-list/filter-editor/filter-editor.component.ts - 248 + 250 Without an owner src/app/components/document-list/filter-editor/filter-editor.component.ts - 251 + 253 @@ -8007,6 +8069,83 @@ 9 + + Equal to + + src/app/data/custom-field-query.ts + 24 + + + + In + + src/app/data/custom-field-query.ts + 25 + + + + Is null + + src/app/data/custom-field-query.ts + 26 + + + + Exists + + src/app/data/custom-field-query.ts + 27 + + + + Contains + + src/app/data/custom-field-query.ts + 28 + + + + Contains (case-insensitive) + + src/app/data/custom-field-query.ts + 29 + + + + Greater than + + src/app/data/custom-field-query.ts + 30 + + + + Greater than or equal to + + src/app/data/custom-field-query.ts + 31 + + + + Less than + + src/app/data/custom-field-query.ts + 32 + + + + Less than or equal to + + src/app/data/custom-field-query.ts + 33 + + + + Range + + src/app/data/custom-field-query.ts + 34 + + Boolean diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 005de5369..93c458ae0 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -108,6 +108,7 @@ import { FileDropComponent } from './components/file-drop/file-drop.component' import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component' import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component' +import { CustomFieldsQueryDropdownComponent } from './components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component' import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component' import { PdfViewerModule } from 'ng2-pdf-viewer' import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component' @@ -141,6 +142,7 @@ import { arrowRightShort, arrowUpRight, asterisk, + braces, bodyText, boxArrowUp, boxArrowUpRight, @@ -198,6 +200,7 @@ import { link, listTask, listUl, + nodePlus, pencil, people, peopleFill, @@ -227,6 +230,7 @@ import { uiRadios, upcScan, x, + xCircle, xLg, } from 'ngx-bootstrap-icons' @@ -242,6 +246,7 @@ const icons = { arrowRightShort, arrowUpRight, asterisk, + braces, bodyText, boxArrowUp, boxArrowUpRight, @@ -299,6 +304,7 @@ const icons = { link, listTask, listUl, + nodePlus, pencil, people, peopleFill, @@ -328,6 +334,7 @@ const icons = { uiRadios, upcScan, x, + xCircle, xLg, } @@ -485,6 +492,7 @@ function initializeApp(settings: SettingsService) { CustomFieldsComponent, CustomFieldEditDialogComponent, CustomFieldsDropdownComponent, + CustomFieldsQueryDropdownComponent, ProfileEditDialogComponent, DocumentLinkComponent, PreviewPopupComponent, diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html new file mode 100644 index 000000000..9da2886f4 --- /dev/null +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html @@ -0,0 +1,163 @@ +
    + +
    +
    + @for (element of selectionModel.queries; track element.id; let i = $index) { +
    + @switch (element.type) { + @case (CustomFieldQueryComponentType.Atom) { + + } + @case (CustomFieldQueryComponentType.Expression) { + + } + } +
    + } +
    +
    +
    + + + @if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Date) { + + + } @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Float || getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Integer) { + + } @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Boolean) { + + } @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Select) { + + } @else { + + } + + + +
    + + + @switch (atom.operator) { + @case (CustomFieldQueryOperator.Exists) { + + } + @case (CustomFieldQueryOperator.IsNull) { + + } + @case (CustomFieldQueryOperator.GreaterThanOrEqual) { + + } + @case (CustomFieldQueryOperator.LessThanOrEqual) { + + } + @case (CustomFieldQueryOperator.GreaterThan) { + + } + @case (CustomFieldQueryOperator.LessThan) { + + } + @case (CustomFieldQueryOperator.Contains) { + + } + @case (CustomFieldQueryOperator.In) { + + } + @case (CustomFieldQueryOperator.Exact) { + + } + @default { + + } + } + +
    +
    + + +
    +
    +
    + + + + + @if (expression.negatable) { + + + } +
    +
    + @for (element of expression.value; track element.id; let i = $index) { +
    + @switch (element.type) { + @case (CustomFieldQueryComponentType.Atom) { + + } + @case (CustomFieldQueryComponentType.Expression) { + + } + } +
    + } +
    +
    +
    + + + @if (expression.depth > 0) { + + } +
    +
    +
    diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss new file mode 100644 index 000000000..a10c4658d --- /dev/null +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss @@ -0,0 +1,43 @@ +.dropdown-menu { + width: 370px; + @media(min-width: 768px) { + width: 600px; + } +} + +::ng-deep .ng-select-container { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + height: 100% !important; +} + +::ng-deep .rounded-end .ng-select-container { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +::ng-deep .ng-select { + max-width: 100px; + min-width: 35%; + font-size: 14px; +} + +::ng-deep .doc-link-select { + padding-top: 0 !important; + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; + background-image: none !important; + + .ng-select-container, + .ng-select.ng-select-opened > .ng-select-container { + border: none !important; + min-height: 34px !important; + background: none !important; + } + .ng-select { + max-width: 200px; + min-width: 140px; + } +} diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts new file mode 100644 index 000000000..e6199c696 --- /dev/null +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts @@ -0,0 +1,320 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { + CustomFieldQueriesModel, + CustomFieldsQueryDropdownComponent, +} from './custom-fields-query-dropdown.component' +import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' +import { of } from 'rxjs' +import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' +import { + CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP, + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperatorGroups, +} from 'src/app/data/custom-field-query' +import { provideHttpClientTesting } from '@angular/common/http/testing' +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { + CustomFieldQueryExpression, + CustomFieldQueryAtom, + CustomFieldQueryElement, +} from 'src/app/utils/custom-field-query-element' + +const customFields = [ + { + id: 1, + name: 'Test Field', + data_type: CustomFieldDataType.String, + extra_data: {}, + }, + { + id: 2, + name: 'Test Select Field', + data_type: CustomFieldDataType.Select, + extra_data: { select_options: ['Option 1', 'Option 2'] }, + }, +] + +describe('CustomFieldsQueryDropdownComponent', () => { + let component: CustomFieldsQueryDropdownComponent + let fixture: ComponentFixture + let customFieldsService: CustomFieldsService + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CustomFieldsQueryDropdownComponent], + imports: [NgbDropdownModule, NgxBootstrapIconsModule.pick(allIcons)], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }).compileComponents() + + customFieldsService = TestBed.inject(CustomFieldsService) + jest.spyOn(customFieldsService, 'listAll').mockReturnValue( + of({ + count: customFields.length, + all: customFields.map((f) => f.id), + results: customFields, + }) + ) + fixture = TestBed.createComponent(CustomFieldsQueryDropdownComponent) + component = fixture.componentInstance + component.icon = 'ui-radios' + fixture.detectChanges() + }) + + it('should initialize custom fields on creation', () => { + expect(component.customFields).toEqual(customFields) + }) + + it('should add an expression when opened if queries are empty', () => { + component.selectionModel.clear() + component.onOpenChange(true) + expect(component.selectionModel.queries.length).toBe(1) + }) + + it('should support reset the selection model', () => { + component.selectionModel.addExpression() + component.reset() + expect(component.selectionModel.isEmpty()).toBeTruthy() + }) + + it('should get operators for a field', () => { + const field: CustomField = { + id: 1, + name: 'Test Field', + data_type: CustomFieldDataType.String, + extra_data: {}, + } + component.customFields = [field] + const operators = component.getOperatorsForField(1) + expect(operators.length).toEqual( + [ + ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[ + CustomFieldQueryOperatorGroups.Basic + ], + ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[ + CustomFieldQueryOperatorGroups.String + ], + ].length + ) + + // Fallback to basic operators if field is not found + const operators2 = component.getOperatorsForField(2) + expect(operators2.length).toEqual( + CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[ + CustomFieldQueryOperatorGroups.Basic + ].length + ) + }) + + it('should get select options for a field', () => { + const field: CustomField = { + id: 1, + name: 'Test Field', + data_type: CustomFieldDataType.Select, + extra_data: { select_options: ['Option 1', 'Option 2'] }, + } + component.customFields = [field] + const options = component.getSelectOptionsForField(1) + expect(options).toEqual(['Option 1', 'Option 2']) + + // Fallback to empty array if field is not found + const options2 = component.getSelectOptionsForField(2) + expect(options2).toEqual([]) + }) + + it('should remove an element from the selection model', () => { + const expression = new CustomFieldQueryExpression() + const atom = new CustomFieldQueryAtom() + ;(expression.value as CustomFieldQueryElement[]).push(atom) + component.selectionModel.addExpression(expression) + component.removeElement(atom) + expect(component.selectionModel.isEmpty()).toBeTruthy() + const expression2 = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [1, 'icontains', 'test'], + [2, 'icontains', 'test'], + ], + ]) + component.selectionModel.addExpression(expression2) + component.removeElement(expression2) + expect(component.selectionModel.isEmpty()).toBeTruthy() + }) + + it('should emit selectionModelChange when model changes', () => { + const nextSpy = jest.spyOn(component.selectionModelChange, 'next') + const atom = new CustomFieldQueryAtom([1, 'icontains', 'test']) + component.selectionModel.addAtom(atom) + atom.changed.next(atom) + expect(nextSpy).toHaveBeenCalled() + }) + + it('should complete selection model subscription when new selection model is set', () => { + const completeSpy = jest.spyOn(component.selectionModel.changed, 'complete') + const selectionModel = new CustomFieldQueriesModel() + component.selectionModel = selectionModel + expect(completeSpy).toHaveBeenCalled() + }) + + it('should support adding an atom', () => { + const expression = new CustomFieldQueryExpression() + component.addAtom(expression) + expect(expression.value.length).toBe(1) + }) + + it('should support adding an expression', () => { + const expression = new CustomFieldQueryExpression() + component.addExpression(expression) + expect(expression.value.length).toBe(1) + }) + + it('should support getting a custom field by ID', () => { + expect(component.getCustomFieldByID(1)).toEqual(customFields[0]) + }) + + it('should sanitize name from title', () => { + component.title = 'Test Title' + expect(component.name).toBe('test_title') + }) + + describe('CustomFieldQueriesModel', () => { + let model: CustomFieldQueriesModel + + beforeEach(() => { + model = new CustomFieldQueriesModel() + }) + + it('should initialize with empty queries', () => { + expect(model.queries).toEqual([]) + }) + + it('should clear queries and fire event', () => { + const nextSpy = jest.spyOn(model.changed, 'next') + model.addExpression() + model.clear() + expect(model.queries).toEqual([]) + expect(nextSpy).toHaveBeenCalledWith(model) + }) + + it('should clear queries without firing event', () => { + const nextSpy = jest.spyOn(model.changed, 'next') + model.addExpression() + model.clear(false) + expect(model.queries).toEqual([]) + expect(nextSpy).not.toHaveBeenCalled() + }) + + it('should validate an empty model as invalid', () => { + expect(model.isValid()).toBeFalsy() + }) + + it('should validate a model with valid expression as valid', () => { + const expression = new CustomFieldQueryExpression() + const atom = new CustomFieldQueryAtom([1, 'icontains', 'test']) + const atom2 = new CustomFieldQueryAtom([2, 'icontains', 'test']) + const expression2 = new CustomFieldQueryExpression() + expression2.addAtom(atom) + expression2.addAtom(atom2) + expression.addExpression(expression2) + model.addExpression(expression) + expect(model.isValid()).toBeTruthy() + }) + + it('should validate a model with invalid expression as invalid', () => { + const expression = new CustomFieldQueryExpression() + model.addExpression(expression) + expect(model.isValid()).toBeFalsy() + }) + + it('should validate an atom with in or contains operator', () => { + const atom = new CustomFieldQueryAtom([1, 'in', '[1,2,3]']) + expect(model['validateAtom'].apply(null, [atom])).toBeTruthy() + atom.operator = 'contains' + atom.value = [1, 2, 3] + expect(model['validateAtom'].apply(null, [atom])).toBeTruthy() + atom.value = null + expect(model['validateAtom'].apply(null, [atom])).toBeFalsy() + }) + + it('should check if model is empty', () => { + expect(model.isEmpty()).toBeTruthy() + model.addExpression() + expect(model.isEmpty()).toBeTruthy() + const atom = new CustomFieldQueryAtom([1, 'icontains', 'test']) + model.addAtom(atom) + expect(model.isEmpty()).toBeFalsy() + }) + + it('should add an atom to the model', () => { + const atom = new CustomFieldQueryAtom([1, 'icontains', 'test']) + model.addAtom(atom) + expect(model.queries.length).toBe(1) + expect( + (model.queries[0] as CustomFieldQueryExpression).value.length + ).toBe(1) + }) + + it('should add an expression to the model, propagate changes', () => { + const expression = new CustomFieldQueryExpression() + model.addExpression(expression) + expect(model.queries.length).toBe(1) + const expression2 = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [1, 'icontains', 'test'], + [2, 'icontains', 'test'], + ], + ]) + model.addExpression(expression2) + const nextSpy = jest.spyOn(model.changed, 'next') + expression2.changed.next(expression2) + expect(nextSpy).toHaveBeenCalled() + }) + + it('should remove an element from the model', () => { + const expression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [1, 'icontains', 'test'], + [2, 'icontains', 'test'], + ], + ]) + const atom = new CustomFieldQueryAtom([1, 'icontains', 'test']) + const expression2 = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [3, 'icontains', 'test'], + [4, 'icontains', 'test'], + ], + ]) + expression.addAtom(atom) + expression2.addExpression(expression) + model.addExpression(expression2) + model.removeElement(atom) + expect(model.queries.length).toBe(1) + model.removeElement(expression2) + }) + + it('should fire changed event when an atom changes', () => { + const nextSpy = jest.spyOn(model.changed, 'next') + const atom = new CustomFieldQueryAtom([1, 'icontains', 'test']) + model.addAtom(atom) + atom.changed.next(atom) + expect(nextSpy).toHaveBeenCalledWith(model) + }) + + it('should complete changed subject when element is removed', () => { + const expression = new CustomFieldQueryExpression() + const atom = new CustomFieldQueryAtom([1, 'icontains', 'test']) + ;(expression.value as CustomFieldQueryElement[]).push(atom) + model.addExpression(expression) + const completeSpy = jest.spyOn(atom.changed, 'complete') + model.removeElement(atom) + expect(completeSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts new file mode 100644 index 000000000..923907158 --- /dev/null +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts @@ -0,0 +1,294 @@ +import { + Component, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core' +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' +import { Subject, first, takeUntil } from 'rxjs' +import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' +import { + CustomFieldQueryElementType, + CustomFieldQueryOperator, + CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE, + CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP, + CustomFieldQueryOperatorGroups, + CUSTOM_FIELD_QUERY_OPERATOR_LABELS, + CUSTOM_FIELD_QUERY_MAX_DEPTH, + CUSTOM_FIELD_QUERY_MAX_ATOMS, +} from 'src/app/data/custom-field-query' +import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' +import { + CustomFieldQueryElement, + CustomFieldQueryExpression, + CustomFieldQueryAtom, +} from 'src/app/utils/custom-field-query-element' +import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options' + +export class CustomFieldQueriesModel { + public queries: CustomFieldQueryElement[] = [] + + public readonly changed = new Subject() + + public clear(fireEvent = true) { + this.queries = [] + if (fireEvent) { + this.changed.next(this) + } + } + + public isValid(): boolean { + return ( + this.queries.length > 0 && + this.validateExpression(this.queries[0] as CustomFieldQueryExpression) + ) + } + + public isEmpty(): boolean { + return ( + this.queries.length === 0 || + (this.queries.length === 1 && this.queries[0].value.length === 0) + ) + } + + private validateAtom(atom: CustomFieldQueryAtom) { + let valid = !!(atom.field && atom.operator && atom.value !== null) + if ( + [ + CustomFieldQueryOperator.In.valueOf(), + CustomFieldQueryOperator.Contains.valueOf(), + ].includes(atom.operator) && + atom.value + ) { + valid = valid && atom.value.length > 0 + } + return valid + } + + private validateExpression(expression: CustomFieldQueryExpression) { + return ( + expression.operator && + expression.value.length > 0 && + (expression.value as CustomFieldQueryElement[]).every((e) => + e.type === CustomFieldQueryElementType.Atom + ? this.validateAtom(e as CustomFieldQueryAtom) + : this.validateExpression(e as CustomFieldQueryExpression) + ) + ) + } + + public addAtom(atom: CustomFieldQueryAtom) { + if (this.queries.length === 0) { + this.addExpression() + } + ;(this.queries[0].value as CustomFieldQueryElement[]).push(atom) + atom.changed.subscribe(() => { + if (atom.field && atom.operator && atom.value) { + this.changed.next(this) + } + }) + } + + public addExpression( + expression: CustomFieldQueryExpression = new CustomFieldQueryExpression() + ) { + if (this.queries.length > 0) { + ;( + (this.queries[0] as CustomFieldQueryExpression) + .value as CustomFieldQueryElement[] + ).push(expression) + } else { + this.queries.push(expression) + } + expression.changed.subscribe(() => { + this.changed.next(this) + }) + } + + private findElement( + queryElement: CustomFieldQueryElement, + elements: any[] + ): CustomFieldQueryElement { + for (let i = 0; i < elements.length; i++) { + if (elements[i] === queryElement) { + return elements.splice(i, 1)[0] + } else if (elements[i].type === CustomFieldQueryElementType.Expression) { + return this.findElement( + queryElement, + elements[i].value as CustomFieldQueryElement[] + ) + } + } + } + + public removeElement(queryElement: CustomFieldQueryElement) { + let foundComponent + for (let i = 0; i < this.queries.length; i++) { + let query = this.queries[i] + if (query === queryElement) { + foundComponent = this.queries.splice(i, 1)[0] + break + } else if (query.type === CustomFieldQueryElementType.Expression) { + foundComponent = this.findElement(queryElement, query.value as any[]) + } + } + if (foundComponent) { + foundComponent.changed.complete() + if (this.isEmpty()) { + this.clear() + } + this.changed.next(this) + } + } +} + +@Component({ + selector: 'pngx-custom-fields-query-dropdown', + templateUrl: './custom-fields-query-dropdown.component.html', + styleUrls: ['./custom-fields-query-dropdown.component.scss'], +}) +export class CustomFieldsQueryDropdownComponent { + public CustomFieldQueryComponentType = CustomFieldQueryElementType + public CustomFieldQueryOperator = CustomFieldQueryOperator + public CustomFieldDataType = CustomFieldDataType + public CUSTOM_FIELD_QUERY_MAX_DEPTH = CUSTOM_FIELD_QUERY_MAX_DEPTH + public CUSTOM_FIELD_QUERY_MAX_ATOMS = CUSTOM_FIELD_QUERY_MAX_ATOMS + public popperOptions = popperOptionsReenablePreventOverflow + + @Input() + title: string + + @Input() + filterPlaceholder: string = '' + + @Input() + icon: string + + @Input() + allowSelectNone: boolean = false + + @Input() + editing = false + + @Input() + applyOnClose = false + + get name(): string { + return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null + } + + @Input() + disabled: boolean = false + + @ViewChild('dropdown') dropdown: NgbDropdown + + private _selectionModel: CustomFieldQueriesModel + + @Input() + set selectionModel(model: CustomFieldQueriesModel) { + if (this._selectionModel) { + this._selectionModel.changed.complete() + } + model.changed.subscribe(() => { + this.onModelChange() + }) + this._selectionModel = model + } + + get selectionModel(): CustomFieldQueriesModel { + return this._selectionModel + } + + private onModelChange() { + if (this.selectionModel.isEmpty() || this.selectionModel.isValid()) { + this.selectionModelChange.next(this.selectionModel) + this.selectionModel.isEmpty() && this.dropdown?.close() + } + } + + @Output() + selectionModelChange = new EventEmitter() + + customFields: CustomField[] = [] + + private unsubscribeNotifier: Subject = new Subject() + + constructor(protected customFieldsService: CustomFieldsService) { + this.selectionModel = new CustomFieldQueriesModel() + this.getFields() + this.reset() + } + + ngOnDestroy(): void { + this.unsubscribeNotifier.next(this) + this.unsubscribeNotifier.complete() + } + + public onOpenChange(open: boolean) { + if (open && this.selectionModel.queries.length === 0) { + this.selectionModel.addExpression() + } + } + + public get isActive(): boolean { + return ( + (this.selectionModel.queries[0] as CustomFieldQueryExpression)?.value + ?.length > 0 + ) + } + + private getFields() { + this.customFieldsService + .listAll() + .pipe(first(), takeUntil(this.unsubscribeNotifier)) + .subscribe((result) => { + this.customFields = result.results + }) + } + + public getCustomFieldByID(id: number): CustomField { + return this.customFields.find((field) => field.id === id) + } + + public addAtom(expression: CustomFieldQueryExpression) { + expression.addAtom() + } + + public addExpression(expression: CustomFieldQueryExpression) { + expression.addExpression() + } + + public removeElement(element: CustomFieldQueryElement) { + this.selectionModel.removeElement(element) + } + + public reset() { + this.selectionModel.clear(false) + this.selectionModel.changed.next(this.selectionModel) + } + + getOperatorsForField( + fieldID: number + ): Array<{ value: string; label: string }> { + const field = this.customFields.find((field) => field.id === fieldID) + const groups: CustomFieldQueryOperatorGroups[] = field + ? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type] + : [CustomFieldQueryOperatorGroups.Basic] + const operators = groups.flatMap( + (group) => CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[group] + ) + return operators.map((operator) => ({ + value: operator, + label: CUSTOM_FIELD_QUERY_OPERATOR_LABELS[operator], + })) + } + + getSelectOptionsForField(fieldID: number): string[] { + const field = this.customFields.find((field) => field.id === fieldID) + if (field) { + return field.extra_data['select_options'] + } + return [] + } +} diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.html b/src-ui/src/app/components/common/input/document-link/document-link.component.html index a8ecce4e6..94f4f21b4 100644 --- a/src-ui/src/app/components/common/input/document-link/document-link.component.html +++ b/src-ui/src/app/components/common/input/document-link/document-link.component.html @@ -1,50 +1,57 @@ -
    -
    -
    - @if (title) { - - } - @if (removable) { - - } -
    -
    -
    - - - - - -
    -
    Loading...
    -
    - -
    {{document.title}} ({{document.created | customDate:'shortDate'}})
    -
    -
    +@if (minimal) { + +} @else { +
    +
    +
    + @if (title) { + + } + @if (removable) { + + } +
    +
    + + @if (hint) { + {{hint}} + }
    - @if (hint) { - {{hint}} - }
    -
    +} + + + + + + + +
    +
    Loading...
    +
    + +
    {{document.title}} ({{document.created | customDate:'shortDate'}})
    +
    +
    +
    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 83a6a742e..882aacad5 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 @@ -46,6 +46,12 @@ export class DocumentLinkComponent @Input() parentDocumentID: number + @Input() + minimal: boolean = false + + @Input() + placeholder: string = $localize`Search for documents` + constructor(private documentsService: DocumentService) { super() } diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index e70f4c710..4eb9d179e 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -140,7 +140,7 @@ } @else { @if (list.displayMode === DisplayMode.LARGE_CARDS) {
    - @for (d of list.documents; track trackByDocumentId($index, d)) { + @for (d of list.documents; track d.id) {
    - @for (d of list.documents; track trackByDocumentId($index, d)) { + @for (d of list.documents; track d.id) { } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts index 557d5f388..9aa876da2 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts @@ -49,16 +49,19 @@ const tags: Tag[] = [ name: 'Tag1 Foo', matching_algorithm: MATCH_LITERAL, match: 'foo', + document_count: 35, }, { id: 2, name: 'Tag2', matching_algorithm: MATCH_NONE, + document_count: 0, }, { id: 3, name: 'Tag3', matching_algorithm: MATCH_AUTO, + document_count: 5, }, ] @@ -180,7 +183,7 @@ describe('ManagementListComponent', () => { const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reloadData') - const editButton = fixture.debugElement.queryAll(By.css('button'))[7] + const editButton = fixture.debugElement.queryAll(By.css('button'))[6] editButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -205,7 +208,7 @@ describe('ManagementListComponent', () => { const deleteSpy = jest.spyOn(tagService, 'delete') const reloadSpy = jest.spyOn(component, 'reloadData') - const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8] + const deleteButton = fixture.debugElement.queryAll(By.css('button'))[7] deleteButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -225,7 +228,7 @@ describe('ManagementListComponent', () => { it('should support quick filter for objects', () => { const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - const filterButton = fixture.debugElement.queryAll(By.css('button'))[6] + const filterButton = fixture.debugElement.queryAll(By.css('button'))[8] filterButton.triggerEventHandler('click') expect(qfSpy).toHaveBeenCalledWith([ { rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() }, diff --git a/src-ui/src/app/data/custom-field.ts b/src-ui/src/app/data/custom-field.ts index 7e52d0785..bca77dd51 100644 --- a/src-ui/src/app/data/custom-field.ts +++ b/src-ui/src/app/data/custom-field.ts @@ -59,4 +59,5 @@ export interface CustomField extends ObjectWithId { select_options?: string[] default_currency?: string } + document_count?: number } diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 30f3dd26d..f326b4eee 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -494,6 +494,8 @@ class CustomFieldSerializer(serializers.ModelSerializer): read_only=False, ) + document_count = serializers.IntegerField(read_only=True) + class Meta: model = CustomField fields = [ @@ -501,6 +503,7 @@ class CustomFieldSerializer(serializers.ModelSerializer): "name", "data_type", "extra_data", + "document_count", ] def validate(self, attrs): diff --git a/src/documents/tests/test_api_custom_fields.py b/src/documents/tests/test_api_custom_fields.py index 6ffe14681..bfe352d56 100644 --- a/src/documents/tests/test_api_custom_fields.py +++ b/src/documents/tests/test_api_custom_fields.py @@ -1,6 +1,7 @@ import json from datetime import date +from django.contrib.auth.models import Permission from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APITestCase @@ -933,3 +934,51 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): results = response.data["results"] self.assertEqual(len(results), 1) self.assertEqual(results[0]["name"], custom_field_int.name) + + def test_custom_fields_document_count(self): + custom_field_string = CustomField.objects.create( + name="Test Custom Field String", + data_type=CustomField.FieldDataType.STRING, + ) + doc = Document.objects.create( + title="WOW", + content="the content", + checksum="123", + mime_type="application/pdf", + owner=self.user, + ) + + response = self.client.get( + f"{self.ENDPOINT}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(results[0]["document_count"], 0) + + CustomFieldInstance.objects.create( + document=doc, + field=custom_field_string, + value_text="test value", + ) + + response = self.client.get( + f"{self.ENDPOINT}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(results[0]["document_count"], 1) + + # Test as user without access to the document + non_superuser = User.objects.create_user(username="non_superuser") + non_superuser.user_permissions.add( + *Permission.objects.all(), + ) + non_superuser.save() + self.client.force_authenticate(user=non_superuser) + self.client.force_login(user=non_superuser) + response = self.client.get( + f"{self.ENDPOINT}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(results[0]["document_count"], 0) diff --git a/src/documents/views.py b/src/documents/views.py index c870c15b5..94674a83f 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1897,6 +1897,32 @@ class CustomFieldViewSet(ModelViewSet): queryset = CustomField.objects.all().order_by("-created") + def get_queryset(self): + filter = ( + Q(fields__document__deleted_at__isnull=True) + if self.request.user is None or self.request.user.is_superuser + else ( + Q( + fields__document__deleted_at__isnull=True, + fields__document__id__in=get_objects_for_user_owner_aware( + self.request.user, + "documents.view_document", + Document, + ).values_list("id", flat=True), + ) + ) + ) + return ( + super() + .get_queryset() + .annotate( + document_count=Count( + "fields", + filter=filter, + ), + ) + ) + class SystemStatusView(PassUserMixin): permission_classes = (IsAuthenticated,) From c7029021cd76e407ea3af2602565ac55a7f85a68 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 4 Oct 2024 23:59:31 -0700 Subject: [PATCH 38/74] Fix: skip accounts without enabled rules --- src/paperless_mail/tasks.py | 4 +++ src/paperless_mail/tests/test_mail.py | 38 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/paperless_mail/tasks.py b/src/paperless_mail/tasks.py index ab013a41e..67bd620cf 100644 --- a/src/paperless_mail/tasks.py +++ b/src/paperless_mail/tasks.py @@ -5,6 +5,7 @@ from celery import shared_task from paperless_mail.mail import MailAccountHandler from paperless_mail.mail import MailError from paperless_mail.models import MailAccount +from paperless_mail.models import MailRule logger = logging.getLogger("paperless.mail.tasks") @@ -13,6 +14,9 @@ logger = logging.getLogger("paperless.mail.tasks") def process_mail_accounts(): total_new_documents = 0 for account in MailAccount.objects.all(): + if not MailRule.objects.filter(account=account, enabled=True).exists(): + logger.info(f"No rules enabled for account {account}. Skipping.") + continue try: total_new_documents += MailAccountHandler().handle_mail_account(account) except MailError: diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index b1e3ff06e..c8a8e5124 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -1543,6 +1543,14 @@ class TestTasks(TestCase): username="A", password="A", ) + MailRule.objects.create( + name="A", + account=MailAccount.objects.get(name="A"), + ) + MailRule.objects.create( + name="B", + account=MailAccount.objects.get(name="B"), + ) result = tasks.process_mail_accounts() @@ -1552,3 +1560,33 @@ class TestTasks(TestCase): m.side_effect = lambda account: 0 result = tasks.process_mail_accounts() self.assertIn("No new", result) + + @mock.patch("paperless_mail.tasks.MailAccountHandler.handle_mail_account") + def test_accounts_no_enabled_rules(self, m): + m.side_effect = lambda account: 6 + + MailAccount.objects.create( + name="A", + imap_server="A", + username="A", + password="A", + ) + MailAccount.objects.create( + name="B", + imap_server="A", + username="A", + password="A", + ) + MailRule.objects.create( + name="A", + account=MailAccount.objects.get(name="A"), + enabled=False, + ) + MailRule.objects.create( + name="B", + account=MailAccount.objects.get(name="B"), + enabled=False, + ) + + tasks.process_mail_accounts() + self.assertEqual(m.call_count, 0) From ca1f21fccaae7947afd68dd198db96e8ea49e5bf Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:54:01 -0700 Subject: [PATCH 39/74] Feature: Enhanced templating for filename format (#7836) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- Pipfile | 1 + Pipfile.lock | 491 +++++++----------- docs/advanced_usage.md | 147 +++++- src-ui/messages.xlf | 42 +- src-ui/src/app/app.module.ts | 2 + .../storage-path-edit-dialog.component.html | 2 +- ...storage-path-edit-dialog.component.spec.ts | 2 + .../storage-path-edit-dialog.component.ts | 4 +- .../input/textarea/textarea.component.html | 33 ++ .../input/textarea/textarea.component.scss | 0 .../input/textarea/textarea.component.spec.ts | 31 ++ .../input/textarea/textarea.component.ts | 27 + .../management-list.component.html | 4 +- .../management-list.component.ts | 2 + .../storage-path-list.component.spec.ts | 14 + .../storage-path-list.component.ts | 4 +- src/documents/checks.py | 18 + src/documents/file_handling.py | 176 ++----- .../migrations/1012_fix_archive_files.py | 34 +- .../migrations/1055_alter_storagepath_path.py | 36 ++ src/documents/models.py | 2 +- src/documents/serialisers.py | 43 +- src/documents/templating/__init__.py | 0 src/documents/templating/filepath.py | 333 ++++++++++++ src/documents/templating/utils.py | 24 + src/documents/tests/test_api_objects.py | 2 +- src/documents/tests/test_checks.py | 16 + src/documents/tests/test_file_handling.py | 394 ++++++++++---- .../test_migration_storage_path_template.py | 30 ++ 29 files changed, 1299 insertions(+), 615 deletions(-) create mode 100644 src-ui/src/app/components/common/input/textarea/textarea.component.html create mode 100644 src-ui/src/app/components/common/input/textarea/textarea.component.scss create mode 100644 src-ui/src/app/components/common/input/textarea/textarea.component.spec.ts create mode 100644 src-ui/src/app/components/common/input/textarea/textarea.component.ts create mode 100644 src/documents/migrations/1055_alter_storagepath_path.py create mode 100644 src/documents/templating/__init__.py create mode 100644 src/documents/templating/filepath.py create mode 100644 src/documents/templating/utils.py create mode 100644 src/documents/tests/test_migration_storage_path_template.py diff --git a/Pipfile b/Pipfile index a872e1184..c2db33487 100644 --- a/Pipfile +++ b/Pipfile @@ -57,6 +57,7 @@ watchdog = "~=4.0" whitenoise = "~=6.7" whoosh = "~=2.7" zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"} +jinja2 = "~=3.1" [dev-packages] # Linting diff --git a/Pipfile.lock b/Pipfile.lock index 47622a94f..675e89c10 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1be8ddf875b6aa77fcf61f5c065c9dc3941cad4b9285ce64da60b5684357dade" + "sha256": "1e113d0879e4e0bc3c384115057647ac8d9be05252dd7c708a1fc873f294ef28" }, "pipfile-spec": 6, "requires": {}, @@ -544,12 +544,12 @@ }, "django-soft-delete": { "hashes": [ - "sha256:428df56ea4fbb13f42d4f752f11f2a517aa31ac3d1b450e6b78c4c5d5d9dfc3b", - "sha256:558821ea988fd69a3a7008cdb33a06ded491af828bdffa5b287fa0fb72b52a09" + "sha256:36cf26a9eaa5f4c0fdb5cb6367ea183e91b7f73783cad173e4071a4747dd1277", + "sha256:fc16c870020984b7f58254adead12fdfb637a6c2f4bd8a93a3a636b18b1463e0" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==1.0.14" + "version": "==1.0.15" }, "djangorestframework": { "hashes": [ @@ -744,11 +744,11 @@ }, "httpcore": { "hashes": [ - "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" + "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", + "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" ], "markers": "python_version >= '3.8'", - "version": "==1.0.5" + "version": "==1.0.6" }, "httptools": { "hashes": [ @@ -828,11 +828,11 @@ }, "imap-tools": { "hashes": [ - "sha256:218ea6495d73275ecc2fa4a34717c137bacf2c4a3d34c9d10a9581a6af1ac94f", - "sha256:4c31e9df1d28149436a86871cf84a0b37221a91521fc1a57897e0a152ee3f6d1" + "sha256:bd84d0f40fbd7be27f6ff5c3908e74d96e99d6b5f44f19cd6e928d308c811916", + "sha256:e657df2f62c1b263c0fd1610cfcd9f8cde26de6b696ae25c401ba75d91a5fd93" ], "index": "pypi", - "version": "==1.7.2" + "version": "==1.7.3" }, "img2pdf": { "hashes": [ @@ -844,7 +844,7 @@ "hashes": [ "sha256:8440ffe49c4ae81a8df57c1ae1eb4b6bfa7acb830099bfb3e305b383005cc128" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1'", "version": "==1.3.5" }, "inotifyrecursive": { @@ -853,9 +853,18 @@ "sha256:a2c450b317693e4538416f90eb1d7858506dafe6b8b885037bd2dd9ae2dafa1e" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1'", "version": "==0.3.5" }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, "joblib": { "hashes": [ "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", @@ -1032,6 +1041,72 @@ "markers": "python_version >= '3.8'", "version": "==3.0.0" }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, "mdurl": { "hashes": [ "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", @@ -1304,54 +1379,49 @@ }, "pikepdf": { "hashes": [ - "sha256:01be001988ce0f6a5a89319f37fc14f27df75c4e332222ed8e993d14405acb02", - "sha256:0759842e47369fe5fa0d61de2ac9ff073895c75567f3efbc4aebc6c1cafee17e", - "sha256:127e94632eb1ccd5d4d859511f084a0a314555cba621595a135915fc9e1710c5", - "sha256:163600dcd8d158e9287934b65a516b469b153859ab029e40fb3a0eff16c7dd7a", - "sha256:1dd707e6159af953f5560138f695b3a1ae2e1a0750535be70a3b75a720279330", - "sha256:1e6b3083ef2e3c29af33fcdb73a9a61a8e4dbe540edb474c19b9866194c6bf25", - "sha256:3c7e5c3a425de7db1fc13583883d2fa10119ce85071cc1d53344383498739254", - "sha256:3efff6ffda819d4193dd8e63c6f304bf85f9ae961c0247dc0b716b7c74fb7094", - "sha256:4a5c5ccccb5812a5be5b5cb66c8c8a6f796910ab89932a3048a4e66e5436bd01", - "sha256:4b9e9416da42da43f386244b2bab2a236830ccb11598b73fcd43d32fd234aaff", - "sha256:4c8bf24b8bf933f4022c6ace5ee757453e3dacb806a8e826461fd5f33ce15a70", - "sha256:531b6685912eb630a7fe57c527c9b5636c50c543eb0cdb5807b139e0d7712696", - "sha256:5e31aeb15ab21ba340a9013c1665e7ce85bd1f8167e6710c455d51f82c2e64e0", - "sha256:61bb9dfe58ee3ee2a286ea4cd21af87e1853a2d1433b550e3f58faa005b6ea3a", - "sha256:6275467b7eacb6fb04f16727e90e6562c6bbf449ece4e57273956beb8f1cdacd", - "sha256:6e15689fd715e83ff555cbdb939a0453c6c94af9975ae9b3292dd68231014653", - "sha256:755f559c206de5b3de0e35430ad28e50f37866d96a41b3ad41d7114660e1c58b", - "sha256:7fa15e5ff3e17dc6295d676d673787c79fec67cca59261a22ccf7604914170b1", - "sha256:8a50c58bee394f69561ab2861f77ce763f91cf7af6c8a1919109bb33fe8ca669", - "sha256:9699fe058b44e59cdcd05bcadf9cfa8f5242b48e44f9a4772bb321cd74d8e339", - "sha256:96ea92374d25481a2213403ae06c990ea41a1f35b0404dd072b7070dac76f41b", - "sha256:98ff348c97c7c641c2d2b741d60c8edf22e0fe76fa5c386cb351a3abd3f2a9b9", - "sha256:a32ef219737e53b48754acb45ad7840aee8403d97fc79539c26501a2d9089c91", - "sha256:aefa94f8ea6371fc3cbf78f55f669efec6e28e317927e8dd8a237e19a7be50fb", - "sha256:baaf78ed49e3cecfc4d30f2c7291d9b19bebe8a5f8e5940d7e7c93683b47a6f9", - "sha256:c1b883e1ebe28fbc318ce5c971b3dca9b30621bc2fe1642c99cda76cf442c4a2", - "sha256:c2c21c6a3d7ec96c7f9627ad61195eadff12659e3e00abe7156c34503189db47", - "sha256:c4eb22efae62b057a31ee4cb5574db8edfe15b185c8e89500eca8157fda15974", - "sha256:c6ea5f623629478abaf1e25b1d0edcaee3d0408fd9061fb4f7dc24fb78a25302", - "sha256:cd73d828799e41ee778606e30efd0c27be1e2420b1ed0c9cbc39299872ceed76", - "sha256:ceeac42bfb7227310e617e871d8f7ae6f304cf2783ca0131f3063c54ee1ecb73", - "sha256:d1a1314e4c4b2a28a1af1e700570b3c32c074cf363425768e8bc9f031438aee3", - "sha256:d209e4a9ba99a4460cf987f6cd8703a8723d8a62fc51451c4c1233eff07db02f", - "sha256:d360e64c31f73b16b78ca1e10e9d96f758b4a3fac195cd35f88a5f213808852e", - "sha256:d37ce8a4ade0cddf3827e13867208ffc8c161d38fdb12250b31e1b8cfa58ab1b", - "sha256:d6f240b0c1da5b6656efa3daa087394ddce5b3ecc411b85efcfd7e7228a1bc26", - "sha256:d9ba6c639faac47a85817854d002e2f57683ffe65388a746af580c4a6521646c", - "sha256:e199833ef11a64f22945a9a98d56a98968e988e407cb20d9fa8b6081075c9604", - "sha256:e1e47e80ecfd77dbfc6c7e807e78e5cce0c10d5bd7804c0d9064429d72af981c", - "sha256:e863185d6abadab140a7c3e152d9227afe495cf97d4738efc280896660249180", - "sha256:eb65a84fff25295707250b49f9e2d1186e9f6b4b7f828a0d9e7e2b65a7af6311", - "sha256:f2e4d5632dc03a41d901e4feee474557145c4906d96cf6e7ae8106a85142d2eb", - "sha256:f3ecbc250254b61de2ca973e3d57acb07720e5a810ee0c81d33b051c76d22208", - "sha256:f6b1ee86850fddaea15afdde394109332f7dc63a156e52fb131f9b647b16f920", - "sha256:fc0deac6dd356ef95fcf42db917cfe2c5375640295609924d4825052c2124509" + "sha256:08d0c72ba70cbe9f45772168e0c922b8d7625899cbfbcbd0dfd1316acff90258", + "sha256:0da5ebba4a31e257ca86a93657a4d47afffeda2ee48cde25227ce43d6dabae13", + "sha256:0f74ba40a3c6f450d19b0958df5c92f84965f4160fd973d4a00f00492093f01c", + "sha256:180e7423f3b517688cf14d6c5537e97a1a9b047421915bb28d3198f881b46f14", + "sha256:18e48cc0359f29b5083bad94237b53d928d8491f7ba5d4a389ca5c366226d766", + "sha256:287206055d2543ee768f85c24146e267c2465c1b2024e37ccf80b5a16674d2a2", + "sha256:344602b23ae6852180587c8e3280719ac31c78a4ca6cf08d8a51467d5f1741ba", + "sha256:363d01aa89f871c12fdc3d08c677456d693028cfb865e314cebe679273a7ebcb", + "sha256:38b3f882351d17f65d38d43d24772cfe471b63dc8c09dad52434c4fe02693e33", + "sha256:3afa0ea7b57a125a7744313b08062e59ecca15b2b3b31d13431244ec99b4d683", + "sha256:3ffc14ad4172f7acd7c1c7eb22eeac66f92c93c83941c63a3b56961602af67d7", + "sha256:40724cb905ce682c97f048e4eb3a728eade6dd1bc64425f3b7bb9872688964ea", + "sha256:4a56b7ccf13817689adb977ba92efa8d567d42a307154acff156179ddb76668b", + "sha256:53202d816838e87ee80c28af695b554e3cbfd5cb3598d7bcfba533f9dbd411e9", + "sha256:58e256aec46ee13256e264bae949e23a98707833fc27a3e3c7172c034d0ab870", + "sha256:5eef37caae6ad7a4baa4a6cdb35690945ee1a83bc0da5bbbf0023bc27d113f9c", + "sha256:663ddb129d823f9e1d1e5b4118906c508b801bf1d86fd8583938f96588bf8dda", + "sha256:689fcd1e89857ddc31191d4cc7a1fab2dbb5ce88c347f4de0db41abb176a11fb", + "sha256:6b905b05fc32c4e279aceb1578d7d917ed9a4e70a8a8e8d1b40ee8afff9d6bfc", + "sha256:7a9a738186b07a1177369713e8003371d0393808e5a62b2af86751dad6684a92", + "sha256:7a9feafdb688e64e4017b4596c3cf90793cd658b53e915e6c5a2668d1b3eb0c9", + "sha256:7ac65c0ace97d995dc7263d2912208ac5310c2f84f42f1fdf043b47d77c01852", + "sha256:7dd4166bb14db7d0711f2a32b21cf479217e34828af435b7ece0fab6ea02664d", + "sha256:8022a925cb2c67a1de3736c19de5d280d43241e1b118f1188b94df07e84c8b8f", + "sha256:80630a897d4203be10861e4e7fca8774cf1a85a1abcc41f978984564fb729ef6", + "sha256:8422a3944187a8d24626812044b6b09c865426e2bf8d0b2ead80f56f609b3345", + "sha256:84555d4039ea10935fa2d0084577de5b81b508b9716ce482163e2dc65db1b180", + "sha256:8dbab43c6a6fa2737df6cfccd049bbe5b762c39809a0b14484d0154f403be4fb", + "sha256:8f1153d3f7be818ba0f9f0875f37ed5203c3d500c33a4058a4d2d0f978d3ce29", + "sha256:906d8afc1aa4f2f7409381a58e158207170f3aeba8ad2aec40072a648e8a2914", + "sha256:98e546120b0d5707836a5ced43b09c086f5866f6eed93cfe4a0555c987fcba6f", + "sha256:9e5bb5e40394d6a15c494469be5026c063676918cbabf48345c7fdf8b2f776f5", + "sha256:a09688758168a86585bb0baeae0a704349285ef40a02da8739be4ad8f4b1aee7", + "sha256:cd796a039cbaddb6106127f210d5f2160654c0e629c1b663f2d9e6f67bba96b8", + "sha256:d0d6b11da16d280f83c5406ae0db03521e613c7758212b9104bad3dbf9bf2098", + "sha256:d96804a7e26e2ff37a9c2d796042754b7cae0668ed118a9185169fe1fc3b18d6", + "sha256:dbe7d9930789ea56e8b38b3b6b2b0b4e1090509825ceb572b906a1d23dea0282", + "sha256:e6bb3466f92b7a741a58fe348285d7bec69ea6102bbe3b2a3f49af0e6f2f3327", + "sha256:ecb8ab93305f07f806399101858ab9ff350c3e1de819d6043b5d54220cf81e71", + "sha256:f54ad2d6d3e4c564bf1f9c33e4165b4c36aea62c49654f356a5570f99b89c647" ], - "markers": "python_version >= '3.8'", - "version": "==9.2.1" + "markers": "python_version >= '3.9'", + "version": "==9.3.0" }, "pillow": { "hashes": [ @@ -1520,7 +1590,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-dotenv": { @@ -1732,108 +1802,17 @@ "hiredis" ], "hashes": [ - "sha256:b756df1e4a3858fcc0ef861f3fc53623a96c41e2b1f5304e09e0fe758d333d40", - "sha256:fd4fccba0d7f6aa48c58a78d76ddb4afc698f5da4a2c1d03d916e4fd7ab88cdd" + "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72", + "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "regex": { "hashes": [ - "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", - "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", - "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", - "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", - "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", - "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", - "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", - "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", - "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", - "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", - "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", - "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", - "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", - "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", - "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", - "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", - "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", - "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", - "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", - "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", - "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", - "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", - "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", - "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", - "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", - "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", - "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", - "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", - "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", - "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", - "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", - "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", - "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", - "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", - "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", - "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", - "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", - "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", - "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", - "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", - "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", - "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", - "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", - "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", - "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", - "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", - "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", - "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", - "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", - "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", - "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", - "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", - "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", - "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", - "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", - "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", - "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", - "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", - "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", - "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", - "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", - "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", - "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", - "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", - "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", - "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", - "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", - "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", - "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", - "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", - "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", - "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", - "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", - "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", - "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", - "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", - "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" + "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a" ], "markers": "python_version >= '3.8'", "version": "==2024.9.11" @@ -1843,7 +1822,6 @@ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.8'", "version": "==2.32.3" }, "requests-oauthlib": { @@ -1855,15 +1833,16 @@ }, "rich": { "hashes": [ - "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", - "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a" + "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", + "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.8.1" + "markers": "python_full_version >= '3.8.0'", + "version": "==13.9.2" }, "scikit-learn": { "hashes": [ "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", + "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", @@ -1877,10 +1856,14 @@ "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9", "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", + "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", + "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7", + "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", + "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a" @@ -2028,7 +2011,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sniffio": { @@ -3057,11 +3040,11 @@ }, "faker": { "hashes": [ - "sha256:bf0207af5777950054a2a3b43f4b5bdc33b585918d2b28f1dab52ac0ffe2bac0", - "sha256:f0a60009150736c1c033bea31aa19ae63071c9dcf10adfaf9f1a87a3add84bc8" + "sha256:dbf81295c948270a9e96cd48a9a3ebec73acac9a153d0c854fbbd0294557609f", + "sha256:e0593931bd7be9a9ea984b5d8c302ef1cec19392585d1e90d444199271d0a94d" ], "markers": "python_version >= '3.8'", - "version": "==30.0.0" + "version": "==30.1.0" }, "filelock": { "hashes": [ @@ -3089,11 +3072,11 @@ }, "httpcore": { "hashes": [ - "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" + "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", + "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" ], "markers": "python_version >= '3.8'", - "version": "==1.0.5" + "version": "==1.0.6" }, "httpx": { "extras": [ @@ -3158,6 +3141,7 @@ "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" ], + "index": "pypi", "markers": "python_version >= '3.7'", "version": "==3.1.4" }, @@ -3519,11 +3503,11 @@ }, "pymdown-extensions": { "hashes": [ - "sha256:2653fb658bca5f278029f8c67a67f0f08b7bd3c657e2630d261ad542e97c4192", - "sha256:e68080eac44634406b31f4aec58fbad17b0ec5fca6b086e29008616d54c3906b" + "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf", + "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049" ], "markers": "python_version >= '3.8'", - "version": "==10.11" + "version": "==10.11.2" }, "pyopenssl": { "hashes": [ @@ -3618,7 +3602,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pywavelets": { @@ -3731,100 +3715,9 @@ }, "regex": { "hashes": [ - "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", - "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", - "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", - "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", - "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", - "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", - "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", - "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", - "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", - "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", - "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", - "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", - "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", - "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", - "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", - "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", - "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", - "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", - "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", - "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", - "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", - "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", - "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", - "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", - "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", - "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", - "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", - "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", - "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", - "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", - "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", - "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", - "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", - "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", - "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", - "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", - "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", - "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", - "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", - "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", - "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", - "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", - "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", - "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", - "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", - "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", - "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", - "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", - "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", - "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", - "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", - "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", - "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", - "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", - "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", - "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", - "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", - "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", - "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", - "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", - "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", - "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", - "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", - "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", - "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", - "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", - "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", - "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", - "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", - "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", - "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", - "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", - "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", - "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", - "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", - "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", - "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" + "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a" ], "markers": "python_version >= '3.8'", "version": "==2024.9.11" @@ -3834,33 +3727,32 @@ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.8'", "version": "==2.32.3" }, "ruff": { "hashes": [ - "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750", - "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa", - "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c", - "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0", - "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f", - "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098", - "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0", - "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f", - "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44", - "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2", - "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a", - "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", - "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb", - "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18", - "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5", - "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce", - "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263", - "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87" + "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", + "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", + "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", + "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", + "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", + "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", + "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", + "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", + "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", + "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", + "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", + "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", + "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", + "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", + "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", + "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", + "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", + "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.6.8" + "version": "==0.6.9" }, "scipy": { "hashes": [ @@ -3921,7 +3813,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sniffio": { @@ -3942,11 +3834,11 @@ }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "version": "==2.0.2" }, "twisted": { "extras": [ @@ -4412,7 +4304,6 @@ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.8'", "version": "==2.32.3" }, "sqlparse": { @@ -4425,11 +4316,11 @@ }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "version": "==2.0.2" }, "types-bleach": { "hashes": [ @@ -4468,11 +4359,11 @@ }, "types-docutils": { "hashes": [ - "sha256:5dd2aa5e2e06fcfa090020bc4115479b4dd28da3329ab708563ee29894bd3c0d", - "sha256:9c8ed6d90583944af00f6b5fa3aecc2101e20672f6b1a4a299c6bf7d1e47084d" + "sha256:0d2ea594576e8d05c4ad83165da64a511e538f6ab405ab8347cd6b636c59f934", + "sha256:9816fb4f33067ed22d24c776a411a430bc19318b1af8f373e5581702a07bc4bc" ], "markers": "python_version >= '3.8'", - "version": "==0.21.0.20240907" + "version": "==0.21.0.20241004" }, "types-html5lib": { "hashes": [ @@ -4519,12 +4410,12 @@ }, "types-python-dateutil": { "hashes": [ - "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6", - "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e" + "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d", + "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.9.0.20240906" + "version": "==2.9.0.20241003" }, "types-pyyaml": { "hashes": [ @@ -4536,12 +4427,12 @@ }, "types-redis": { "hashes": [ - "sha256:0e7537e5c085fe96b7d468d5edae0cf667b4ba4b62c6e4a5dfc340bd3b868c23", - "sha256:4bab1a378dbf23c2c95c370dfdb89a8f033957c4fd1a53fee71b529c182fe008" + "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", + "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.6.0.20240903" + "version": "==4.6.0.20241004" }, "types-requests": { "hashes": [ diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index fe8d2e305..398176267 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -265,7 +265,7 @@ This variable allows you to configure the filename (folders are allowed) using placeholders. For example, configuring this to ```bash -PAPERLESS_FILENAME_FORMAT={created_year}/{correspondent}/{title} +PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }} ``` will create a directory structure as follows: @@ -298,39 +298,39 @@ will create a directory structure as follows: when changing `PAPERLESS_FILENAME_FORMAT` you will need to manually run the [`document renamer`](administration.md#renamer) to move any existing documents. -#### Placeholders +### Placeholders {#filename-format-variables} -Paperless provides the following placeholders within filenames: +Paperless provides the following variables for use within filenames: -- `{asn}`: The archive serial number of the document, or "none". -- `{correspondent}`: The name of the correspondent, or "none". -- `{document_type}`: The name of the document type, or "none". -- `{tag_list}`: A comma separated list of all tags assigned to the +- `{{ asn }}`: The archive serial number of the document, or "none". +- `{{ correspondent }}`: The name of the correspondent, or "none". +- `{{ document_type }}`: The name of the document type, or "none". +- `{{ tag_list }}`: A comma separated list of all tags assigned to the document. -- `{title}`: The title of the document. -- `{created}`: The full date (ISO format) the document was created. -- `{created_year}`: Year created only, formatted as the year with +- `{{ title }}`: The title of the document. +- `{{ created }}`: The full date (ISO format) the document was created. +- `{{ created_year }}`: Year created only, formatted as the year with century. -- `{created_year_short}`: Year created only, formatted as the year +- `{{ created_year_short }}`: Year created only, formatted as the year without century, zero padded. -- `{created_month}`: Month created only (number 01-12). -- `{created_month_name}`: Month created name, as per locale -- `{created_month_name_short}`: Month created abbreviated name, as per +- `{{ created_month }}`: Month created only (number 01-12). +- `{{ created_month_name }}`: Month created name, as per locale +- `{{ created_month_name_short }}`: Month created abbreviated name, as per locale -- `{created_day}`: Day created only (number 01-31). -- `{added}`: The full date (ISO format) the document was added to +- `{{ created_day }}`: Day created only (number 01-31). +- `{{ added }}`: The full date (ISO format) the document was added to paperless. -- `{added_year}`: Year added only. -- `{added_year_short}`: Year added only, formatted as the year without +- `{{ added_year }}`: Year added only. +- `{{ added_year_short }}`: Year added only, formatted as the year without century, zero padded. -- `{added_month}`: Month added only (number 01-12). -- `{added_month_name}`: Month added name, as per locale -- `{added_month_name_short}`: Month added abbreviated name, as per +- `{{ added_month }}`: Month added only (number 01-12). +- `{{ added_month_name }}`: Month added name, as per locale +- `{{ added_month_name_short }}`: Month added abbreviated name, as per locale -- `{added_day}`: Day added only (number 01-31). -- `{owner_username}`: Username of document owner, if any, or "none" -- `{original_name}`: Document original filename, minus the extension, if any, or "none" -- `{doc_pk}`: The paperless identifier (primary key) for the document. +- `{{ added_day }}`: Day added only (number 01-31). +- `{{ owner_username }}`: Username of document owner, if any, or "none" +- `{{ original_name }}`: Document original filename, minus the extension, if any, or "none" +- `{{ doc_pk }}`: The paperless identifier (primary key) for the document. !!! warning @@ -338,6 +338,11 @@ Paperless provides the following placeholders within filenames: you may run into the limits of your operating system's maximum path lengths. In that case, files will retain the previous path instead and the issue logged. +!!! tip + + These variables are all simple strings, but the format can be a full template. + See [Filename Templates](#filename-templates) for even more advanced formatting. + Paperless will try to conserve the information from your database as much as possible. However, some characters that you can use in document titles and correspondent names (such as `: \ /` and a couple more) are @@ -363,7 +368,7 @@ paperless will fall back to using the default naming scheme instead. However, keep in mind that inside docker, if files get stored outside of the predefined volumes, they will be lost after a restart. -##### Empty placeholders +#### Empty placeholders You can affect how empty placeholders are treated by changing the [`PAPERLESS_FILENAME_FORMAT_REMOVE_NONE`](configuration.md#PAPERLESS_FILENAME_FORMAT_REMOVE_NONE) setting. @@ -390,8 +395,8 @@ For example, you could define the following two storage paths: the correspondence. ``` -By Year = {created_year}/{correspondent}/{title} -Insurances = Insurances/{correspondent}/{created_year}-{created_month}-{created_day} {title} +By Year = {{ created_year }}/{{ correspondent }}/{{ title }} +Insurances = Insurances/{{ correspondent }}/{{ created_year }}-{{ created_month }}-{{ created_day }} {{ title }} ``` If you then map these storage paths to the documents, you might get the @@ -418,6 +423,92 @@ Insurances/ # Insurances Defining a storage path is optional. If no storage path is defined for a document, the global [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) is applied. +### Filename Templates {#filename-templates} + +The filename formatting uses [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/) to build the filename. +This allows for complex logic to be included in the format, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures) +and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11) to manipulate the [variables](#filename-format-variables) +provided. The template is provided as a string, potentially multiline, and rendered into a single line. + +In addition, the entire Document instance is available to be utilized in a more advanced way, as well as some variables which only make sense to be accessed +with more complex logic. + +#### Additional Variables + +- `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string +- `{{ custom_fields }}`: A mapping of custom field names to their type and value. A user can access the mapping by field name or check if a field is applied by checking its existence in the variable. + +!!! tip + + To access a custom field which has a space in the name, use the `get_cf_value` filter. See the examples below. + This helps get fields by name and handle a default value if the named field is not attached to a Document. + +#### Examples + +This example will construct a path based on the archive serial number range: + +```jinja +somepath/ +{% if document.archive_serial_number >= 0 and document.archive_serial_number <= 200 %} + asn-000-200/{{title}} +{% elif document.archive_serial_number >= 201 and document.archive_serial_number <= 400 %} + asn-201-400 + {% if document.archive_serial_number >= 201 and document.archive_serial_number < 300 %} + /asn-2xx + {% elif document.archive_serial_number >= 300 and document.archive_serial_number < 400 %} + /asn-3xx + {% endif %} +{% endif %} +/{{ title }} +``` + +For a document with an ASN of 205, it would result in `somepath/asn-201-400/asn-2xx/Title.pdf`, but +a document with an ASN of 355 would be placed in `somepath/asn-201-400/asn-3xx/Title.pdf`. + +```jinja +{% if document.mime_type == "application/pdf" %} + pdfs +{% elif document.mime_type == "image/png" %} + pngs +{% else %} + others +{% endif %} +/{{ title }} +``` + +For a PDF document, it would result in `pdfs/Title.pdf`, but for a PNG document, the path would be `pngs/Title.pdf`. + +To use custom fields: + +```jinja +{% if "Invoice" in custom_fields %} + invoices/{{ custom_fields.Invoice.value }} +{% else %} + not-invoices/{{ title }} +{% endif %} +``` + +If the document has a custom field named "Invoice" with a value of 123, it would be filed into the `invoices/123.pdf`, but a document without the custom field +would be filed to `not-invoices/Title.pdf` + +If the custom field is named "Invoice Number", you would access the value of it via the `get_cf_value` filter due to quirks of the Django Template Language: + +```jinja +"invoices/{{ custom_fields|get_cf_value('Invoice Number') }}" +``` + +You can also use a custom `datetime` filter to format dates: + +```jinja +invoices/ +{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%Y') }}/ +{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%m') }}/ +{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%d') }}/ +Invoice_{{ custom_fields|get_cf_value("Select Field") }}_{{ custom_fields|get_cf_value("Date Field","2024-01-01")|replace("-", "") }}.pdf +``` + +This will create a path like `invoices/2022/01/01/Invoice_OptionTwo_20220101.pdf` if the custom field "Date Field" is set to January 1, 2022 and "Select Field" is set to `OptionTwo`. + ## Automatic recovery of invalid PDFs {#pdf-recovery} Paperless will attempt to "clean" certain invalid PDFs with `qpdf` before processing if, for example, the mime_type diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 3570a77c1..3d8d89c55 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -1569,7 +1569,7 @@ src/app/components/manage/management-list/management-list.component.ts - 208 + 210 src/app/components/manage/workflows/workflows.component.html @@ -2193,11 +2193,11 @@ src/app/components/manage/management-list/management-list.component.ts - 204 + 206 src/app/components/manage/management-list/management-list.component.ts - 321 + 323 @@ -2239,7 +2239,7 @@ src/app/components/manage/management-list/management-list.component.ts - 323 + 325 src/app/components/manage/workflows/workflows.component.ts @@ -2594,7 +2594,7 @@ src/app/components/manage/management-list/management-list.component.ts - 325 + 327 src/app/components/manage/workflows/workflows.component.ts @@ -4776,6 +4776,10 @@ src/app/components/common/input/text/text.component.html 9 + + src/app/components/common/input/textarea/textarea.component.html + 9 + src/app/components/common/input/url/url.component.html 7 @@ -7774,7 +7778,7 @@ src/app/components/manage/management-list/management-list.component.ts - 308 + 310 @@ -7857,7 +7861,7 @@ Automatic src/app/components/manage/management-list/management-list.component.ts - 115 + 117 src/app/data/matching-model.ts @@ -7868,7 +7872,7 @@ None src/app/components/manage/management-list/management-list.component.ts - 117 + 119 src/app/data/matching-model.ts @@ -7879,70 +7883,70 @@ Successfully created . src/app/components/manage/management-list/management-list.component.ts - 161 + 163 Error occurred while creating . src/app/components/manage/management-list/management-list.component.ts - 166 + 168 Successfully updated . src/app/components/manage/management-list/management-list.component.ts - 181 + 183 Error occurred while saving . src/app/components/manage/management-list/management-list.component.ts - 186 + 188 Associated documents will not be deleted. src/app/components/manage/management-list/management-list.component.ts - 206 + 208 Error while deleting element src/app/components/manage/management-list/management-list.component.ts - 222 + 224 Permissions updated successfully src/app/components/manage/management-list/management-list.component.ts - 301 + 303 This operation will permanently delete all objects. src/app/components/manage/management-list/management-list.component.ts - 322 + 324 Objects deleted successfully src/app/components/manage/management-list/management-list.component.ts - 336 + 338 Error deleting objects src/app/components/manage/management-list/management-list.component.ts - 342 + 344 @@ -7963,7 +7967,7 @@ Do you really want to delete the storage path ""? src/app/components/manage/storage-path-list/storage-path-list.component.ts - 52 + 54 diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 93c458ae0..5b9460617 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -41,6 +41,7 @@ import { DocumentCardSmallComponent } from './components/document-list/document- 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 { TextAreaComponent } from './components/common/input/textarea/textarea.component' import { SelectComponent } from './components/common/input/select/select.component' import { CheckComponent } from './components/common/input/check/check.component' import { UrlComponent } from './components/common/input/url/url.component' @@ -440,6 +441,7 @@ function initializeApp(settings: SettingsService) { DocumentCardSmallComponent, BulkEditorComponent, TextComponent, + TextAreaComponent, SelectComponent, CheckComponent, UrlComponent, diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html index ecec393c9..f8232f957 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html @@ -10,7 +10,7 @@ @for (column of extraColumns; track column) { - + } @@ -64,7 +64,7 @@ @for (column of extraColumns; track column) { -
    ASNCorrespondentTitleTagsOwnerNotesDocument typeStorage pathCreatedAdded
    Pages - Shared - - {{getDisplayCustomFieldTitle(field)}} - CorrespondentTitleTagsOwnerNotesDocument typeStorage pathCreatedAddedPages + Shared + + {{getDisplayCustomFieldTitle(field)}} +
    @@ -364,7 +364,7 @@ } @if (list.displayMode === DisplayMode.SMALL_CARDS) {
    - @for (d of list.documents; track trackByDocumentId($index, d)) { + @for (d of list.documents; track d.id) { 0) { - + > } { ToggleableDropdownButtonComponent, DatesDropdownComponent, CustomDatePipe, + CustomFieldsQueryDropdownComponent, ], imports: [ RouterModule, @@ -190,6 +198,7 @@ describe('FilterEditorComponent', () => { NgbDatepickerModule, NgxBootstrapIconsModule.pick(allIcons), NgbTypeaheadModule, + NgSelectModule, ], providers: [ FilterPipe, @@ -838,108 +847,79 @@ describe('FilterEditorComponent', () => { ] })) - it('should ingest filter rules for has all custom fields', fakeAsync(() => { - expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength( - 0 - ) + it('should ingest filter rules for custom fields all', fakeAsync(() => { + expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy() component.filterRules = [ { rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, - value: '42', - }, - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, - value: '43', + value: '42,43', }, ] - expect(component.customFieldSelectionModel.logicalOperator).toEqual( - LogicalOperator.And + expect(component.customFieldQueriesModel.queries[0].operator).toEqual( + CustomFieldQueryLogicalOperator.And ) - expect(component.customFieldSelectionModel.getSelectedItems()).toEqual( - custom_fields - ) - // coverage - component.filterRules = [ - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, - value: null, - }, - ] - component.toggleTag(2) // coverage + expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(2) + expect( + ( + component.customFieldQueriesModel.queries[0] + .value[0] as CustomFieldQueryAtom + ).serialize() + ).toEqual(['42', CustomFieldQueryOperator.Exists, 'true']) })) it('should ingest filter rules for has any custom fields', fakeAsync(() => { - expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength( - 0 - ) + expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy() component.filterRules = [ { rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY, - value: '42', - }, - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY, - value: '43', + value: '42,43', }, ] - expect(component.customFieldSelectionModel.logicalOperator).toEqual( - LogicalOperator.Or + expect(component.customFieldQueriesModel.queries[0].operator).toEqual( + CustomFieldQueryLogicalOperator.Or ) - expect(component.customFieldSelectionModel.getSelectedItems()).toEqual( - custom_fields - ) - // coverage - component.filterRules = [ - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY, - value: null, - }, - ] + expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(2) + expect( + ( + component.customFieldQueriesModel.queries[0] + .value[0] as CustomFieldQueryAtom + ).serialize() + ).toEqual(['42', CustomFieldQueryOperator.Exists, 'true']) })) - it('should ingest filter rules for has any custom field', fakeAsync(() => { - expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength( - 0 - ) + it('should ingest filter rules for custom field queries', fakeAsync(() => { + expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy() component.filterRules = [ { - rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS, - value: '1', + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: '["AND", [[42, "exists", "true"],[43, "exists", "true"]]]', }, ] - expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength( - 1 + expect(component.customFieldQueriesModel.queries[0].operator).toEqual( + CustomFieldQueryLogicalOperator.And ) - expect(component.customFieldSelectionModel.get(null)).toBeTruthy() - })) + expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(2) + expect( + ( + component.customFieldQueriesModel.queries[0] + .value[0] as CustomFieldQueryAtom + ).serialize() + ).toEqual([42, CustomFieldQueryOperator.Exists, 'true']) - it('should ingest filter rules for exclude tag(s)', fakeAsync(() => { - expect(component.customFieldSelectionModel.getExcludedItems()).toHaveLength( - 0 - ) + // atom component.filterRules = [ { - rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - value: '42', - }, - { - rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - value: '43', - }, - ] - expect(component.customFieldSelectionModel.logicalOperator).toEqual( - LogicalOperator.And - ) - expect(component.customFieldSelectionModel.getExcludedItems()).toEqual( - custom_fields - ) - // coverage - component.filterRules = [ - { - rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - value: null, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: '[42, "exists", "true"]', }, ] + expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(1) + expect( + ( + component.customFieldQueriesModel.queries[0] + .value[0] as CustomFieldQueryAtom + ).serialize() + ).toEqual([42, CustomFieldQueryOperator.Exists, 'true']) })) it('should ingest filter rules for owner', fakeAsync(() => { @@ -1453,71 +1433,37 @@ describe('FilterEditorComponent', () => { ]) })) - it('should convert user input to correct filter rules on custom field select not assigned', fakeAsync(() => { - const customFieldsFilterableDropdown = fixture.debugElement.queryAll( - By.directive(FilterableDropdownComponent) - )[4] - customFieldsFilterableDropdown.triggerEventHandler('opened') - const customFieldButton = customFieldsFilterableDropdown.queryAll( - By.directive(ToggleableDropdownButtonComponent) - )[0] - customFieldButton.triggerEventHandler('toggle') - fixture.detectChanges() - expect(component.filterRules).toEqual([ - { - rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS, - value: 'false', - }, - ]) - })) - it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => { - const customFieldsFilterableDropdown = fixture.debugElement.queryAll( - By.directive(FilterableDropdownComponent) - )[4] // CF dropdown - customFieldsFilterableDropdown.triggerEventHandler('opened') - const customFieldButtons = customFieldsFilterableDropdown.queryAll( - By.directive(ToggleableDropdownButtonComponent) + const customFieldsQueryDropdown = fixture.debugElement.queryAll( + By.directive(CustomFieldsQueryDropdownComponent) + )[0] + const customFieldToggleButton = customFieldsQueryDropdown.query( + By.css('button') ) - customFieldButtons[1].triggerEventHandler('toggle') - customFieldButtons[2].triggerEventHandler('toggle') + customFieldToggleButton.triggerEventHandler('click') fixture.detectChanges() - expect(component.filterRules).toEqual([ - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, - value: custom_fields[0].id.toString(), - }, - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, - value: custom_fields[1].id.toString(), - }, - ]) - const toggleOperatorButtons = customFieldsFilterableDropdown.queryAll( - By.css('input[type=radio]') + const customFieldButtons = customFieldsQueryDropdown.queryAll( + By.css('button') ) - toggleOperatorButtons[1].nativeElement.checked = true - toggleOperatorButtons[1].triggerEventHandler('change') + customFieldButtons[1].triggerEventHandler('click') fixture.detectChanges() + const query = component.customFieldQueriesModel + .queries[0] as CustomFieldQueryAtom + query.field = custom_fields[0].id + const fieldSelect: NgSelectComponent = customFieldsQueryDropdown.queryAll( + By.directive(NgSelectComponent) + )[0].componentInstance + fieldSelect.open() + const options = customFieldsQueryDropdown.queryAll(By.css('.ng-option')) + options[0].nativeElement.click() + expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(1) expect(component.filterRules).toEqual([ { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY, - value: custom_fields[0].id.toString(), - }, - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY, - value: custom_fields[1].id.toString(), - }, - ]) - customFieldButtons[2].triggerEventHandler('exclude') - fixture.detectChanges() - expect(component.filterRules).toEqual([ - { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, - value: custom_fields[0].id.toString(), - }, - { - rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - value: custom_fields[1].id.toString(), + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify([ + CustomFieldQueryLogicalOperator.Or, + [[custom_fields[0].id, 'exists', 'true']], + ]), }, ]) })) @@ -1930,21 +1876,11 @@ describe('FilterEditorComponent', () => { component.filterRules = [ { - rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, - value: '42', + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: '["AND",[["42","exists","true"],["43","exists","true"]]]', }, ] - expect(component.generateFilterName()).toEqual( - `Custom fields: ${custom_fields[0].name}` - ) - - component.filterRules = [ - { - rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS, - value: 'false', - }, - ] - expect(component.generateFilterName()).toEqual('Without any custom field') + expect(component.generateFilterName()).toEqual(`Custom fields query`) component.filterRules = [ { diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts index fe1f6cc8c..24ef1b347 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -12,7 +12,7 @@ import { import { Tag } from 'src/app/data/tag' import { Correspondent } from 'src/app/data/correspondent' import { DocumentType } from 'src/app/data/document-type' -import { Observable, Subject, Subscription, from } from 'rxjs' +import { Observable, Subject, from } from 'rxjs' import { catchError, debounceTime, @@ -62,7 +62,7 @@ import { FILTER_HAS_CUSTOM_FIELDS_ANY, FILTER_HAS_CUSTOM_FIELDS_ALL, FILTER_HAS_ANY_CUSTOM_FIELDS, - FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, + FILTER_CUSTOM_FIELDS_QUERY, } from 'src/app/data/filter-rule-type' import { FilterableDropdownSelectionModel, @@ -92,6 +92,15 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomField } from 'src/app/data/custom-field' import { SearchService } from 'src/app/services/rest/search.service' +import { + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from 'src/app/data/custom-field-query' +import { CustomFieldQueriesModel } from '../../common/custom-fields-query-dropdown/custom-fields-query-dropdown.component' +import { + CustomFieldQueryExpression, + CustomFieldQueryAtom, +} from 'src/app/utils/custom-field-query-element' const TEXT_FILTER_TARGET_TITLE = 'title' const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content' @@ -225,15 +234,8 @@ export class FilterEditorComponent return $localize`Without any tag` } - case FILTER_HAS_CUSTOM_FIELDS_ALL: - return $localize`Custom fields: ${ - this.customFields.find((f) => f.id == +rule.value)?.name - }` - - case FILTER_HAS_ANY_CUSTOM_FIELDS: - if (rule.value == 'false') { - return $localize`Without any custom field` - } + case FILTER_CUSTOM_FIELDS_QUERY: + return $localize`Custom fields query` case FILTER_TITLE: return $localize`Title: ${rule.value}` @@ -321,7 +323,7 @@ export class FilterEditorComponent correspondentSelectionModel = new FilterableDropdownSelectionModel() documentTypeSelectionModel = new FilterableDropdownSelectionModel() storagePathSelectionModel = new FilterableDropdownSelectionModel() - customFieldSelectionModel = new FilterableDropdownSelectionModel() + customFieldQueriesModel = new CustomFieldQueriesModel() dateCreatedBefore: string dateCreatedAfter: string @@ -356,7 +358,7 @@ export class FilterEditorComponent this.storagePathSelectionModel.clear(false) this.tagSelectionModel.clear(false) this.correspondentSelectionModel.clear(false) - this.customFieldSelectionModel.clear(false) + this.customFieldQueriesModel.clear(false) this._textFilter = null this._moreLikeId = null this.dateAddedBefore = null @@ -523,34 +525,45 @@ export class FilterEditorComponent false ) break + case FILTER_CUSTOM_FIELDS_QUERY: + try { + const query = JSON.parse(rule.value) + if (Array.isArray(query)) { + if (query.length === 2) { + // expression + this.customFieldQueriesModel.addExpression( + new CustomFieldQueryExpression(query as any) + ) + } else if (query.length === 3) { + // atom + this.customFieldQueriesModel.addAtom( + new CustomFieldQueryAtom(query as any) + ) + } + } + } catch (e) { + // error handled by list view service + } + break + // Legacy custom field filters case FILTER_HAS_CUSTOM_FIELDS_ALL: - this.customFieldSelectionModel.logicalOperator = LogicalOperator.And - this.customFieldSelectionModel.set( - rule.value ? +rule.value : null, - ToggleableItemState.Selected, - false + this.customFieldQueriesModel.addExpression( + new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + rule.value + .split(',') + .map((id) => [id, CustomFieldQueryOperator.Exists, 'true']), + ]) ) break case FILTER_HAS_CUSTOM_FIELDS_ANY: - this.customFieldSelectionModel.logicalOperator = LogicalOperator.Or - this.customFieldSelectionModel.set( - rule.value ? +rule.value : null, - ToggleableItemState.Selected, - false - ) - break - case FILTER_HAS_ANY_CUSTOM_FIELDS: - this.customFieldSelectionModel.set( - null, - ToggleableItemState.Selected, - false - ) - break - case FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS: - this.customFieldSelectionModel.set( - rule.value ? +rule.value : null, - ToggleableItemState.Excluded, - false + this.customFieldQueriesModel.addExpression( + new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.Or, + rule.value + .split(',') + .map((id) => [id, CustomFieldQueryOperator.Exists, 'true']), + ]) ) break case FILTER_ASN_ISNULL: @@ -768,34 +781,14 @@ export class FilterEditorComponent }) }) } - if (this.customFieldSelectionModel.isNoneSelected()) { + let queries = this.customFieldQueriesModel.queries.map((query) => + query.serialize() + ) + if (queries.length > 0) { filterRules.push({ - rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS, - value: 'false', + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify(queries[0]), }) - } else { - const customFieldFilterType = - this.customFieldSelectionModel.logicalOperator == LogicalOperator.And - ? FILTER_HAS_CUSTOM_FIELDS_ALL - : FILTER_HAS_CUSTOM_FIELDS_ANY - this.customFieldSelectionModel - .getSelectedItems() - .filter((field) => field.id) - .forEach((field) => { - filterRules.push({ - rule_type: customFieldFilterType, - value: field.id?.toString(), - }) - }) - this.customFieldSelectionModel - .getExcludedItems() - .filter((field) => field.id) - .forEach((field) => { - filterRules.push({ - rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - value: field.id?.toString(), - }) - }) } if (this.dateCreatedBefore) { filterRules.push({ @@ -1079,10 +1072,6 @@ export class FilterEditorComponent this.storagePathSelectionModel.apply() } - onCustomFieldsDropdownOpen() { - this.customFieldSelectionModel.apply() - } - updateTextFilter(text, updateRules = true) { this._textFilter = text if (updateRules) { diff --git a/src-ui/src/app/data/custom-field-query.ts b/src-ui/src/app/data/custom-field-query.ts new file mode 100644 index 000000000..226a10605 --- /dev/null +++ b/src-ui/src/app/data/custom-field-query.ts @@ -0,0 +1,127 @@ +import { CustomFieldDataType } from './custom-field' + +export enum CustomFieldQueryLogicalOperator { + And = 'AND', + Or = 'OR', + Not = 'NOT', +} + +export enum CustomFieldQueryOperator { + Exact = 'exact', + In = 'in', + IsNull = 'isnull', + Exists = 'exists', + Contains = 'contains', + IContains = 'icontains', + GreaterThan = 'gt', + GreaterThanOrEqual = 'gte', + LessThan = 'lt', + LessThanOrEqual = 'lte', + Range = 'range', +} + +export const CUSTOM_FIELD_QUERY_OPERATOR_LABELS = { + [CustomFieldQueryOperator.Exact]: $localize`Equal to`, + [CustomFieldQueryOperator.In]: $localize`In`, + [CustomFieldQueryOperator.IsNull]: $localize`Is null`, + [CustomFieldQueryOperator.Exists]: $localize`Exists`, + [CustomFieldQueryOperator.Contains]: $localize`Contains`, + [CustomFieldQueryOperator.IContains]: $localize`Contains (case-insensitive)`, + [CustomFieldQueryOperator.GreaterThan]: $localize`Greater than`, + [CustomFieldQueryOperator.GreaterThanOrEqual]: $localize`Greater than or equal to`, + [CustomFieldQueryOperator.LessThan]: $localize`Less than`, + [CustomFieldQueryOperator.LessThanOrEqual]: $localize`Less than or equal to`, + [CustomFieldQueryOperator.Range]: $localize`Range`, +} + +export enum CustomFieldQueryOperatorGroups { + Basic = 'basic', + String = 'string', + Arithmetic = 'arithmetic', + Containment = 'containment', + Subset = 'subset', + Date = 'date', +} + +// Modified from filters.py > SUPPORTED_EXPR_OPERATORS +export const CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP = { + [CustomFieldQueryOperatorGroups.Basic]: [ + CustomFieldQueryOperator.Exists, + CustomFieldQueryOperator.IsNull, + CustomFieldQueryOperator.Exact, + ], + [CustomFieldQueryOperatorGroups.String]: [CustomFieldQueryOperator.IContains], + [CustomFieldQueryOperatorGroups.Arithmetic]: [ + CustomFieldQueryOperator.GreaterThan, + CustomFieldQueryOperator.GreaterThanOrEqual, + CustomFieldQueryOperator.LessThan, + CustomFieldQueryOperator.LessThanOrEqual, + ], + [CustomFieldQueryOperatorGroups.Containment]: [ + CustomFieldQueryOperator.Contains, + ], + [CustomFieldQueryOperatorGroups.Subset]: [CustomFieldQueryOperator.In], + [CustomFieldQueryOperatorGroups.Date]: [ + CustomFieldQueryOperator.GreaterThanOrEqual, + CustomFieldQueryOperator.LessThanOrEqual, + ], +} + +// filters.py > SUPPORTED_EXPR_CATEGORIES +export const CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE = { + [CustomFieldDataType.String]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.String, + ], + [CustomFieldDataType.Url]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.String, + ], + [CustomFieldDataType.Date]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Date, + ], + [CustomFieldDataType.Boolean]: [CustomFieldQueryOperatorGroups.Basic], + [CustomFieldDataType.Integer]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Arithmetic, + ], + [CustomFieldDataType.Float]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Arithmetic, + ], + [CustomFieldDataType.Monetary]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.String, + CustomFieldQueryOperatorGroups.Arithmetic, + ], + [CustomFieldDataType.DocumentLink]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Containment, + ], + [CustomFieldDataType.Select]: [ + CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Subset, + ], +} + +export const CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR = { + [CustomFieldQueryOperator.Exact]: 'string|boolean', + [CustomFieldQueryOperator.IsNull]: 'boolean', + [CustomFieldQueryOperator.Exists]: 'boolean', + [CustomFieldQueryOperator.IContains]: 'string', + [CustomFieldQueryOperator.GreaterThanOrEqual]: 'string|number', + [CustomFieldQueryOperator.LessThanOrEqual]: 'string|number', + [CustomFieldQueryOperator.GreaterThan]: 'number', + [CustomFieldQueryOperator.LessThan]: 'number', + [CustomFieldQueryOperator.Contains]: 'array', + [CustomFieldQueryOperator.In]: 'array', +} + +export const CUSTOM_FIELD_QUERY_MAX_DEPTH = 4 +export const CUSTOM_FIELD_QUERY_MAX_ATOMS = 5 + +export enum CustomFieldQueryElementType { + Atom = 'Atom', + Expression = 'Expression', +} diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index 9a87a421c..1c6b1cdf8 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -55,6 +55,8 @@ export const FILTER_HAS_CUSTOM_FIELDS_ANY = 39 export const FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS = 40 export const FILTER_HAS_ANY_CUSTOM_FIELDS = 41 +export const FILTER_CUSTOM_FIELDS_QUERY = 42 + export const FILTER_RULE_TYPES: FilterRuleType[] = [ { id: FILTER_TITLE, @@ -317,6 +319,12 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ multi: false, default: true, }, + { + id: FILTER_CUSTOM_FIELDS_QUERY, + filtervar: 'custom_field_query', + datatype: 'string', + multi: false, + }, ] export interface FilterRuleType { diff --git a/src-ui/src/app/utils/custom-field-query-element.spec.ts b/src-ui/src/app/utils/custom-field-query-element.spec.ts new file mode 100644 index 000000000..65be3738a --- /dev/null +++ b/src-ui/src/app/utils/custom-field-query-element.spec.ts @@ -0,0 +1,245 @@ +import { + CustomFieldQueryElement, + CustomFieldQueryAtom, + CustomFieldQueryExpression, +} from './custom-field-query-element' +import { + CustomFieldQueryElementType, + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from '../data/custom-field-query' +import { fakeAsync, tick } from '@angular/core/testing' + +describe('CustomFieldQueryElement', () => { + it('should initialize with correct type and id', () => { + const element = new CustomFieldQueryElement( + CustomFieldQueryElementType.Atom + ) + expect(element.type).toBe(CustomFieldQueryElementType.Atom) + expect(element.id).toBeDefined() + }) + + it('should trigger changed on operator change', () => { + const element = new CustomFieldQueryElement( + CustomFieldQueryElementType.Atom + ) + element.changed.subscribe((changedElement) => { + expect(changedElement).toBe(element) + }) + element.operator = CustomFieldQueryOperator.Exists + }) + + it('should trigger changed subject on value change', () => { + const element = new CustomFieldQueryElement( + CustomFieldQueryElementType.Atom + ) + element.changed.subscribe((changedElement) => { + expect(changedElement).toBe(element) + }) + element.value = 'new value' + }) + + it('should throw error on serialize call', () => { + const element = new CustomFieldQueryElement( + CustomFieldQueryElementType.Atom + ) + expect(() => element.serialize()).toThrow('Implemented in subclass') + }) +}) + +describe('CustomFieldQueryAtom', () => { + it('should initialize with correct field, operator, and value', () => { + const atom = new CustomFieldQueryAtom([1, 'operator', 'value']) + expect(atom.field).toBe(1) + expect(atom.operator).toBe('operator') + expect(atom.value).toBe('value') + }) + + it('should trigger changed subject on field change', () => { + const atom = new CustomFieldQueryAtom() + atom.changed.subscribe((changedAtom) => { + expect(changedAtom).toBe(atom) + }) + atom.field = 2 + }) + + it('should set value to null if operator is not found in CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR', () => { + const atom = new CustomFieldQueryAtom() + atom.operator = 'nonexistent_operator' + expect(atom.value).toBeNull() + }) + + it('should set value to empty string if new type is string', () => { + const atom = new CustomFieldQueryAtom() + atom.operator = CustomFieldQueryOperator.IContains + expect(atom.value).toBe('') + }) + + it('should set value to "true" if new type is boolean', () => { + const atom = new CustomFieldQueryAtom() + atom.operator = CustomFieldQueryOperator.Exists + expect(atom.value).toBe('true') + }) + + it('should set value to empty array if new type is array', () => { + const atom = new CustomFieldQueryAtom() + atom.operator = CustomFieldQueryOperator.In + expect(atom.value).toEqual([]) + }) + + it('should try to set existing value to number if new type is number', () => { + const atom = new CustomFieldQueryAtom() + atom.value = '42' + atom.operator = CustomFieldQueryOperator.GreaterThan + expect(atom.value).toBe('42') + + // fallback to null if value is not parseable + atom.value = 'not_a_number' + atom.operator = CustomFieldQueryOperator.GreaterThan + expect(atom.value).toBeNull() + }) + + it('should change boolean values to empty string if operator is not boolean', () => { + const atom = new CustomFieldQueryAtom() + atom.value = 'true' + atom.operator = CustomFieldQueryOperator.Exact + expect(atom.value).toBe('') + }) + + it('should serialize correctly', () => { + const atom = new CustomFieldQueryAtom([1, 'operator', 'value']) + expect(atom.serialize()).toEqual([1, 'operator', 'value']) + }) + + it('should emit changed on value change after debounce', fakeAsync(() => { + const atom = new CustomFieldQueryAtom() + const changeSpy = jest.spyOn(atom.changed, 'next') + atom.value = 'new value' + tick(1000) + expect(changeSpy).toHaveBeenCalled() + })) +}) + +describe('CustomFieldQueryExpression', () => { + it('should initialize with default operator and empty value', () => { + const expression = new CustomFieldQueryExpression() + expect(expression.operator).toBe(CustomFieldQueryLogicalOperator.Or) + expect(expression.value).toEqual([]) + }) + + it('should initialize with correct operator and value, propagate changes', () => { + const expression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [1, 'exists', 'true'], + [2, 'exists', 'true'], + ], + ]) + expect(expression.operator).toBe(CustomFieldQueryLogicalOperator.And) + expect(expression.value.length).toBe(2) + + // propagate changes + const expressionChangeSpy = jest.spyOn(expression.changed, 'next') + ;(expression.value[0] as CustomFieldQueryAtom).changed.next( + expression.value[0] as any + ) + expect(expressionChangeSpy).toHaveBeenCalled() + + const expression2 = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.Not, + [[CustomFieldQueryLogicalOperator.Or, []]], + ]) + const expressionChangeSpy2 = jest.spyOn(expression2.changed, 'next') + ;(expression2.value[0] as CustomFieldQueryExpression).changed.next( + expression2.value[0] as any + ) + expect(expressionChangeSpy2).toHaveBeenCalled() + }) + + it('should initialize with a sub-expression i.e. NOT', () => { + const expression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.Not, + [ + 'AND', + [ + [1, 'exists', 'true'], + [2, 'exists', 'true'], + ], + ], + ]) + expect(expression.value).toHaveLength(1) + const changedSpy = jest.spyOn(expression.changed, 'next') + ;(expression.value[0] as CustomFieldQueryExpression).changed.next( + expression.value[0] as any + ) + expect(changedSpy).toHaveBeenCalled() + }) + + it('should add atom correctly, propagate changes', () => { + const expression = new CustomFieldQueryExpression() + const atom = new CustomFieldQueryAtom([ + 1, + CustomFieldQueryOperator.Exists, + 'true', + ]) + expression.addAtom(atom) + expect(expression.value).toContain(atom) + const changeSpy = jest.spyOn(expression.changed, 'next') + atom.changed.next(atom) + expect(changeSpy).toHaveBeenCalled() + // coverage + expression.addAtom() + }) + + it('should add expression correctly, propagate changes', () => { + const expression = new CustomFieldQueryExpression() + const subExpression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.Or, + [], + ]) + expression.addExpression(subExpression) + expect(expression.value).toContain(subExpression) + const changeSpy = jest.spyOn(expression.changed, 'next') + subExpression.changed.next(subExpression) + expect(changeSpy).toHaveBeenCalled() + // coverage + expression.addExpression() + }) + + it('should serialize correctly', () => { + const expression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [[1, 'exists', 'true']], + ]) + expect(expression.serialize()).toEqual([ + CustomFieldQueryLogicalOperator.And, + [[1, 'exists', 'true']], + ]) + }) + + it('should serialize NOT expressions correctly', () => { + const expression = new CustomFieldQueryExpression() + expression.addExpression( + new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [1, 'exists', 'true'], + [2, 'exists', 'true'], + ], + ]) + ) + expression.operator = CustomFieldQueryLogicalOperator.Not + const serialized = expression.serialize() + expect(serialized[0]).toBe(CustomFieldQueryLogicalOperator.Not) + expect(serialized[1][0]).toBe(CustomFieldQueryLogicalOperator.And) + expect(serialized[1][1].length).toBe(2) + }) + + it('should be negatable if it has one child which is an expression', () => { + const expression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.Not, + [[CustomFieldQueryLogicalOperator.Or, []]], + ]) + expect(expression.negatable).toBe(true) + }) +}) diff --git a/src-ui/src/app/utils/custom-field-query-element.ts b/src-ui/src/app/utils/custom-field-query-element.ts new file mode 100644 index 000000000..696853f12 --- /dev/null +++ b/src-ui/src/app/utils/custom-field-query-element.ts @@ -0,0 +1,210 @@ +import { Subject, debounceTime, distinctUntilChanged } from 'rxjs' +import { v4 as uuidv4 } from 'uuid' +import { + CustomFieldQueryElementType, + CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR, + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from '../data/custom-field-query' + +export class CustomFieldQueryElement { + public readonly type: CustomFieldQueryElementType + public changed: Subject + protected valueModelChanged: Subject< + string | string[] | number[] | CustomFieldQueryElement[] + > + public depth: number = 0 + public id: string = uuidv4() + + constructor(type: CustomFieldQueryElementType) { + this.type = type + this.changed = new Subject() + this.valueModelChanged = new Subject() + this.connectValueModelChanged() + } + + protected connectValueModelChanged() { + // Allows overriding in subclasses + this.valueModelChanged.subscribe(() => { + this.changed.next(this) + }) + } + + public serialize() { + throw new Error('Implemented in subclass') + } + + protected _operator: string = null + public set operator(value: string) { + this._operator = value + this.changed.next(this) + } + public get operator(): string { + return this._operator + } + + protected _value: string | string[] | number[] | CustomFieldQueryElement[] = + null + public set value( + value: string | string[] | number[] | CustomFieldQueryElement[] + ) { + this._value = value + this.valueModelChanged.next(value) + } + public get value(): string | string[] | number[] | CustomFieldQueryElement[] { + return this._value + } +} + +export class CustomFieldQueryAtom extends CustomFieldQueryElement { + protected _field: number + set field(field: any) { + this._field = parseInt(field, 10) + this.changed.next(this) + } + get field(): number { + return this._field + } + + override set operator(operator: string) { + const newTypes: string[] = + CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR[operator]?.split('|') + if (!newTypes) { + this.value = null + } else { + if (!newTypes.includes(typeof this.value)) { + switch (newTypes[0]) { + case 'string': + this.value = '' + break + case 'boolean': + this.value = 'true' + break + case 'array': + this.value = [] + break + case 'number': + const num = parseFloat(this.value as string) + this.value = isNaN(num) ? null : num.toString() + break + } + } else if ( + ['true', 'false'].includes(this.value as string) && + newTypes.includes('string') + ) { + this.value = '' + } + } + super.operator = operator + } + + override get operator(): string { + // why? + return super.operator + } + + constructor(queryArray: [number, string, string] = [null, null, null]) { + super(CustomFieldQueryElementType.Atom) + ;[this._field, this._operator, this._value] = queryArray + } + + protected override connectValueModelChanged(): void { + this.valueModelChanged + .pipe(debounceTime(1000), distinctUntilChanged()) + .subscribe(() => { + this.changed.next(this) + }) + } + + public override serialize() { + return [this._field, this._operator, this._value] + } +} + +export class CustomFieldQueryExpression extends CustomFieldQueryElement { + protected _value: string[] | number[] | CustomFieldQueryElement[] + + constructor( + expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [ + CustomFieldQueryLogicalOperator.Or, + null, + ] + ) { + super(CustomFieldQueryElementType.Expression) + let values + ;[this._operator, values] = expressionArray + if (!values || values.length === 0) { + this._value = [] + } else if (values?.length > 0 && values[0] instanceof Array) { + this._value = values.map((value) => { + if (value.length === 3) { + const atom = new CustomFieldQueryAtom(value) + atom.depth = this.depth + 1 + atom.changed.subscribe(() => { + this.changed.next(this) + }) + return atom + } else { + const expression = new CustomFieldQueryExpression(value) + expression.depth = this.depth + 1 + expression.changed.subscribe(() => { + this.changed.next(this) + }) + return expression + } + }) + } else { + const expression = new CustomFieldQueryExpression(values as any) + expression.depth = this.depth + 1 + expression.changed.subscribe(() => { + this.changed.next(this) + }) + this._value = [expression] + } + } + + public override serialize() { + let value + value = this._value.map((element) => element.serialize()) + // If the expression is negated it should have only one child which is an expression + if ( + this._operator === CustomFieldQueryLogicalOperator.Not && + value.length === 1 + ) { + value = value[0] + } + return [this._operator, value] + } + + public addAtom( + atom: CustomFieldQueryAtom = new CustomFieldQueryAtom([ + null, + CustomFieldQueryOperator.Exists, + 'true', + ]) + ) { + atom.depth = this.depth + 1 + ;(this._value as CustomFieldQueryElement[]).push(atom) + atom.changed.subscribe(() => { + this.changed.next(this) + }) + } + + public addExpression( + expression: CustomFieldQueryExpression = new CustomFieldQueryExpression() + ) { + expression.depth = this.depth + 1 + ;(this._value as CustomFieldQueryElement[]).push(expression) + expression.changed.subscribe(() => { + this.changed.next(this) + }) + } + + public get negatable(): boolean { + return ( + this.value.length === 1 && + (this.value[0] as CustomFieldQueryElement).type === + CustomFieldQueryElementType.Expression + ) + } +} diff --git a/src-ui/src/app/utils/query-params.spec.ts b/src-ui/src/app/utils/query-params.spec.ts index a1bc0cdcd..64a89efec 100644 --- a/src-ui/src/app/utils/query-params.spec.ts +++ b/src-ui/src/app/utils/query-params.spec.ts @@ -2,13 +2,17 @@ import { convertToParamMap } from '@angular/router' import { FilterRule } from '../data/filter-rule' import { FILTER_CORRESPONDENT, + FILTER_CUSTOM_FIELDS_QUERY, FILTER_HAS_ANY_TAG, + FILTER_HAS_CUSTOM_FIELDS_ALL, + FILTER_HAS_CUSTOM_FIELDS_ANY, FILTER_HAS_TAGS_ALL, } from '../data/filter-rule-type' -import { paramsToViewState } from './query-params' +import { paramsToViewState, transformLegacyFilterRules } from './query-params' import { paramsFromViewState } from './query-params' import { queryParamsFromFilterRules } from './query-params' import { filterRulesFromQueryParams } from './query-params' +import { CustomFieldQueryLogicalOperator } from '../data/custom-field-query' const tags__id__all = '9' const filterRules: FilterRule[] = [ @@ -193,4 +197,58 @@ describe('QueryParams Utils', () => { }, ]) }) + + it('should transform legacy filter rules', () => { + let filterRules: FilterRule[] = [ + { + rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY, + value: '1', + }, + { + rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY, + value: '2', + }, + ] + + let transformedFilterRules = transformLegacyFilterRules(filterRules) + + expect(transformedFilterRules).toEqual([ + { + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify([ + CustomFieldQueryLogicalOperator.Or, + [ + [1, 'exists', true], + [2, 'exists', true], + ], + ]), + }, + ]) + + filterRules = [ + { + rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, + value: '3', + }, + { + rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL, + value: '4', + }, + ] + + transformedFilterRules = transformLegacyFilterRules(filterRules) + + expect(transformedFilterRules).toEqual([ + { + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify([ + CustomFieldQueryLogicalOperator.And, + [ + [3, 'exists', true], + [4, 'exists', true], + ], + ]), + }, + ]) + }) }) diff --git a/src-ui/src/app/utils/query-params.ts b/src-ui/src/app/utils/query-params.ts index 1121bd6a3..608d4edfb 100644 --- a/src-ui/src/app/utils/query-params.ts +++ b/src-ui/src/app/utils/query-params.ts @@ -1,7 +1,17 @@ import { ParamMap, Params } from '@angular/router' import { FilterRule } from '../data/filter-rule' -import { FilterRuleType, FILTER_RULE_TYPES } from '../data/filter-rule-type' +import { + FilterRuleType, + FILTER_RULE_TYPES, + FILTER_HAS_CUSTOM_FIELDS_ANY, + FILTER_CUSTOM_FIELDS_QUERY, + FILTER_HAS_CUSTOM_FIELDS_ALL, +} from '../data/filter-rule-type' import { ListViewState } from '../services/document-list-view.service' +import { + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from '../data/custom-field-query' const SORT_FIELD_PARAMETER = 'sort' const SORT_REVERSE_PARAMETER = 'reverse' @@ -40,6 +50,49 @@ export function paramsToViewState(queryParams: ParamMap): ListViewState { } } +export function transformLegacyFilterRules( + filterRules: FilterRule[] +): FilterRule[] { + const LEGACY_CUSTOM_FIELD_FILTER_RULE_TYPES = [ + FILTER_HAS_CUSTOM_FIELDS_ANY, + FILTER_HAS_CUSTOM_FIELDS_ALL, + ] + if ( + filterRules.filter((rule) => + LEGACY_CUSTOM_FIELD_FILTER_RULE_TYPES.includes(rule.rule_type) + ).length + ) { + const anyRules = filterRules.filter( + (rule) => rule.rule_type === FILTER_HAS_CUSTOM_FIELDS_ANY + ) + const allRules = filterRules.filter( + (rule) => rule.rule_type === FILTER_HAS_CUSTOM_FIELDS_ALL + ) + const customFieldQueryLogicalOperator = allRules.length + ? CustomFieldQueryLogicalOperator.And + : CustomFieldQueryLogicalOperator.Or + const valueRules = allRules.length ? allRules : anyRules + const customFieldQueryExpression = [ + customFieldQueryLogicalOperator, + [ + ...valueRules.map((rule) => [ + parseInt(rule.value), + CustomFieldQueryOperator.Exists, + true, + ]), + ], + ] + filterRules.push({ + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify(customFieldQueryExpression), + }) + } + // TODO: can we support FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS or FILTER_HAS_ANY_CUSTOM_FIELDS? + return filterRules.filter( + (rule) => !LEGACY_CUSTOM_FIELD_FILTER_RULE_TYPES.includes(rule.rule_type) + ) +} + export function filterRulesFromQueryParams( queryParams: ParamMap ): FilterRule[] { @@ -77,7 +130,9 @@ export function filterRulesFromQueryParams( }) ) }) - + filterRulesFromQueryParams = transformLegacyFilterRules( + filterRulesFromQueryParams + ) return filterRulesFromQueryParams } diff --git a/src/documents/filters.py b/src/documents/filters.py index 25e840141..f0a9a55b3 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -29,13 +29,15 @@ from documents.models import Log from documents.models import ShareLink from documents.models import StoragePath from documents.models import Tag -from paperless import settings CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"] ID_KWARGS = ["in", "exact"] INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"] DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"] +CUSTOM_FIELD_QUERY_MAX_DEPTH = 10 +CUSTOM_FIELD_QUERY_MAX_ATOMS = 20 + class CorrespondentFilterSet(FilterSet): class Meta: @@ -234,19 +236,13 @@ def handle_validation_prefix(func: Callable): return wrapper -class CustomFieldLookupParser: +class CustomFieldQueryParser: EXPR_BY_CATEGORY = { "basic": ["exact", "in", "isnull", "exists"], "string": [ - "iexact", - "contains", "icontains", - "startswith", "istartswith", - "endswith", "iendswith", - "regex", - "iregex", ], "arithmetic": [ "gt", @@ -258,23 +254,6 @@ class CustomFieldLookupParser: "containment": ["contains"], } - # These string lookup expressions are problematic. We shall disable - # them by default unless the user explicitly opts in. - STR_EXPR_DISABLED_BY_DEFAULT = [ - # SQLite: is case-sensitive outside the ASCII range - "iexact", - # SQLite: behaves the same as icontains - "contains", - # SQLite: behaves the same as istartswith - "startswith", - # SQLite: behaves the same as iendswith - "endswith", - # Syntax depends on database backends, can be exploited for ReDoS - "regex", - # Syntax depends on database backends, can be exploited for ReDoS - "iregex", - ] - SUPPORTED_EXPR_CATEGORIES = { CustomField.FieldDataType.STRING: ("basic", "string"), CustomField.FieldDataType.URL: ("basic", "string"), @@ -282,7 +261,7 @@ class CustomFieldLookupParser: CustomField.FieldDataType.BOOL: ("basic",), CustomField.FieldDataType.INT: ("basic", "arithmetic"), CustomField.FieldDataType.FLOAT: ("basic", "arithmetic"), - CustomField.FieldDataType.MONETARY: ("basic", "string"), + CustomField.FieldDataType.MONETARY: ("basic", "string", "arithmetic"), CustomField.FieldDataType.DOCUMENTLINK: ("basic", "containment"), CustomField.FieldDataType.SELECT: ("basic",), } @@ -371,7 +350,7 @@ class CustomFieldLookupParser: elif len(expr) == 3: return self._parse_atom(*expr) raise serializers.ValidationError( - [_("Invalid custom field lookup expression")], + [_("Invalid custom field query expression")], ) @handle_validation_prefix @@ -416,13 +395,7 @@ class CustomFieldLookupParser: self._atom_count += 1 if self._atom_count > self._max_atom_count: raise serializers.ValidationError( - [ - _( - "Maximum number of query conditions exceeded. You can raise " - "the limit by setting PAPERLESS_CUSTOM_FIELD_LOOKUP_MAX_ATOMS " - "in your configuration file.", - ), - ], + [_("Maximum number of query conditions exceeded.")], ) custom_field = self._get_custom_field(id_or_name, validation_prefix="0") @@ -444,6 +417,11 @@ class CustomFieldLookupParser: value_field_name = CustomFieldInstance.get_value_field_name( custom_field.data_type, ) + if ( + custom_field.data_type == CustomField.FieldDataType.MONETARY + and op in self.EXPR_BY_CATEGORY["arithmetic"] + ): + value_field_name = "value_monetary_amount" has_field = Q(custom_fields__field=custom_field) # Our special exists operator. @@ -494,22 +472,6 @@ class CustomFieldLookupParser: # Check if the operator is supported for the current data_type. supported = False for category in self.SUPPORTED_EXPR_CATEGORIES[custom_field.data_type]: - if ( - category == "string" - and op in self.STR_EXPR_DISABLED_BY_DEFAULT - and op not in settings.CUSTOM_FIELD_LOOKUP_OPT_IN - ): - raise serializers.ValidationError( - [ - _( - "{expr!r} is disabled by default because it does not " - "behave consistently across database backends, or can " - "cause security risks. If you understand the implications " - "you may enabled it by adding it to " - "`PAPERLESS_CUSTOM_FIELD_LOOKUP_OPT_IN`.", - ).format(expr=op), - ], - ) if op in self.EXPR_BY_CATEGORY[category]: supported = True break @@ -527,7 +489,7 @@ class CustomFieldLookupParser: if not supported: raise serializers.ValidationError( [ - _("{data_type} does not support lookup expr {expr!r}.").format( + _("{data_type} does not support query expr {expr!r}.").format( data_type=custom_field.data_type, expr=raw_op, ), @@ -548,7 +510,7 @@ class CustomFieldLookupParser: custom_field.data_type == CustomField.FieldDataType.DATE and prefix in self.DATE_COMPONENTS ): - # DateField admits lookups in the form of `year__exact`, etc. These take integers. + # DateField admits queries in the form of `year__exact`, etc. These take integers. field = serializers.IntegerField() elif custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK: # We can be more specific here and make sure the value is a list. @@ -610,7 +572,7 @@ class CustomFieldLookupParser: custom_fields__value_document_ids__isnull=False, ) - # First we lookup reverse links from the requested documents. + # First we look up reverse links from the requested documents. links = CustomFieldInstance.objects.filter( document_id__in=value, field__data_type=CustomField.FieldDataType.DOCUMENTLINK, @@ -635,22 +597,14 @@ class CustomFieldLookupParser: # guard against queries that are too deeply nested self._current_depth += 1 if self._current_depth > self._max_query_depth: - raise serializers.ValidationError( - [ - _( - "Maximum nesting depth exceeded. You can raise the limit " - "by setting PAPERLESS_CUSTOM_FIELD_LOOKUP_MAX_DEPTH in " - "your configuration file.", - ), - ], - ) + raise serializers.ValidationError([_("Maximum nesting depth exceeded.")]) try: yield finally: self._current_depth -= 1 -class CustomFieldLookupFilter(Filter): +class CustomFieldQueryFilter(Filter): def __init__(self, validation_prefix): """ A filter that filters documents based on custom field name and value. @@ -665,10 +619,10 @@ class CustomFieldLookupFilter(Filter): if not value: return qs - parser = CustomFieldLookupParser( + parser = CustomFieldQueryParser( self._validation_prefix, - max_query_depth=settings.CUSTOM_FIELD_LOOKUP_MAX_DEPTH, - max_atom_count=settings.CUSTOM_FIELD_LOOKUP_MAX_ATOMS, + max_query_depth=CUSTOM_FIELD_QUERY_MAX_DEPTH, + max_atom_count=CUSTOM_FIELD_QUERY_MAX_ATOMS, ) q, annotations = parser.parse(value) @@ -722,7 +676,7 @@ class DocumentFilterSet(FilterSet): exclude=True, ) - custom_field_lookup = CustomFieldLookupFilter("custom_field_lookup") + custom_field_query = CustomFieldQueryFilter("custom_field_query") shared_by__id = SharedByUser() diff --git a/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py b/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py new file mode 100644 index 000000000..92d45de33 --- /dev/null +++ b/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py @@ -0,0 +1,95 @@ +# Generated by Django 5.1.1 on 2024-09-29 16:26 + +import django.db.models.functions.comparison +import django.db.models.functions.text +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1053_document_page_count"), + ] + + operations = [ + migrations.AddField( + model_name="customfieldinstance", + name="value_monetary_amount", + field=models.GeneratedField( + db_persist=True, + expression=models.Case( + models.When( + then=django.db.models.functions.comparison.Cast( + django.db.models.functions.text.Substr("value_monetary", 1), + output_field=models.DecimalField( + decimal_places=2, + max_digits=65, + ), + ), + value_monetary__regex="^\\d+", + ), + default=django.db.models.functions.comparison.Cast( + django.db.models.functions.text.Substr("value_monetary", 4), + output_field=models.DecimalField( + decimal_places=2, + max_digits=65, + ), + ), + output_field=models.DecimalField(decimal_places=2, max_digits=65), + ), + output_field=models.DecimalField(decimal_places=2, max_digits=65), + ), + ), + migrations.AlterField( + model_name="savedviewfilterrule", + name="rule_type", + field=models.PositiveIntegerField( + choices=[ + (0, "title contains"), + (1, "content contains"), + (2, "ASN is"), + (3, "correspondent is"), + (4, "document type is"), + (5, "is in inbox"), + (6, "has tag"), + (7, "has any tag"), + (8, "created before"), + (9, "created after"), + (10, "created year is"), + (11, "created month is"), + (12, "created day is"), + (13, "added before"), + (14, "added after"), + (15, "modified before"), + (16, "modified after"), + (17, "does not have tag"), + (18, "does not have ASN"), + (19, "title or content contains"), + (20, "fulltext query"), + (21, "more like this"), + (22, "has tags in"), + (23, "ASN greater than"), + (24, "ASN less than"), + (25, "storage path is"), + (26, "has correspondent in"), + (27, "does not have correspondent in"), + (28, "has document type in"), + (29, "does not have document type in"), + (30, "has storage path in"), + (31, "does not have storage path in"), + (32, "owner is"), + (33, "has owner in"), + (34, "does not have owner"), + (35, "does not have owner in"), + (36, "has custom field value"), + (37, "is shared by me"), + (38, "has custom fields"), + (39, "has custom field in"), + (40, "does not have custom field in"), + (41, "does not have custom field"), + (42, "custom fields query"), + ], + verbose_name="rule type", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 6dae8ba65..80476bffa 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -22,6 +22,9 @@ from multiselectfield import MultiSelectField if settings.AUDIT_LOG_ENABLED: from auditlog.registry import auditlog +from django.db.models import Case +from django.db.models.functions import Cast +from django.db.models.functions import Substr from django_softdelete.models import SoftDeleteModel from documents.data_models import DocumentSource @@ -519,6 +522,7 @@ class SavedViewFilterRule(models.Model): (39, _("has custom field in")), (40, _("does not have custom field in")), (41, _("does not have custom field")), + (42, _("custom fields query")), ] saved_view = models.ForeignKey( @@ -921,6 +925,27 @@ class CustomFieldInstance(models.Model): value_monetary = models.CharField(null=True, max_length=128) + value_monetary_amount = models.GeneratedField( + expression=Case( + # If the value starts with a number and no currency symbol, use the whole string + models.When( + value_monetary__regex=r"^\d+", + then=Cast( + Substr("value_monetary", 1), + output_field=models.DecimalField(decimal_places=2, max_digits=65), + ), + ), + # If the value starts with a 3-char currency symbol, use the rest of the string + default=Cast( + Substr("value_monetary", 4), + output_field=models.DecimalField(decimal_places=2, max_digits=65), + ), + output_field=models.DecimalField(decimal_places=2, max_digits=65), + ), + output_field=models.DecimalField(decimal_places=2, max_digits=65), + db_persist=True, + ) + value_document_ids = models.JSONField(null=True) value_select = models.PositiveSmallIntegerField(null=True) diff --git a/src/documents/tests/test_api_filter_by_custom_fields.py b/src/documents/tests/test_api_filter_by_custom_fields.py index 0f7da0a61..c9a0cdcfc 100644 --- a/src/documents/tests/test_api_filter_by_custom_fields.py +++ b/src/documents/tests/test_api_filter_by_custom_fields.py @@ -1,11 +1,9 @@ import json -import re from collections.abc import Callable from datetime import date from unittest.mock import Mock from urllib.parse import quote -import pytest from django.contrib.auth.models import User from rest_framework.test import APITestCase @@ -13,7 +11,6 @@ from documents.models import CustomField from documents.models import Document from documents.serialisers import DocumentSerializer from documents.tests.utils import DirectoriesMixin -from paperless import settings class DocumentWrapper: @@ -31,11 +28,7 @@ class DocumentWrapper: return self._document.custom_fields.get(field__name=custom_field).value -def string_expr_opted_in(op): - return op in settings.CUSTOM_FIELD_LOOKUP_OPT_IN - - -class TestDocumentSearchApi(DirectoriesMixin, APITestCase): +class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): def setUp(self): super().setUp() @@ -111,6 +104,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): self._create_document(monetary_field="USD100.00") self._create_document(monetary_field="USD1.00") self._create_document(monetary_field="EUR50.00") + self._create_document(monetary_field="101.00") # CustomField.FieldDataType.DOCUMENTLINK self._create_document(documentlink_field=None) @@ -188,7 +182,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): "/api/documents/?" + "&".join( ( - f"custom_field_lookup={query_string}", + f"custom_field_query={query_string}", "ordering=archive_serial_number", "page=1", f"page_size={len(self.documents)}", @@ -212,7 +206,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): "/api/documents/?" + "&".join( ( - f"custom_field_lookup={query_string}", + f"custom_field_query={query_string}", "ordering=archive_serial_number", "page=1", f"page_size={len(self.documents)}", @@ -313,32 +307,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): # ==========================================================# # Expressions for string, URL, and monetary fields # # ==========================================================# - @pytest.mark.skipif( - not string_expr_opted_in("iexact"), - reason="iexact expr is disabled.", - ) - def test_iexact(self): - self._assert_query_match_predicate( - ["string_field", "iexact", "paperless"], - lambda document: "string_field" in document - and document["string_field"] is not None - and document["string_field"].lower() == "paperless", - ) - - @pytest.mark.skipif( - not string_expr_opted_in("contains"), - reason="contains expr is disabled.", - ) - def test_contains(self): - # WARNING: SQLite treats "contains" as "icontains"! - # You should avoid "contains" unless you know what you are doing! - self._assert_query_match_predicate( - ["string_field", "contains", "aper"], - lambda document: "string_field" in document - and document["string_field"] is not None - and "aper" in document["string_field"], - ) - def test_icontains(self): self._assert_query_match_predicate( ["string_field", "icontains", "aper"], @@ -347,20 +315,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): and "aper" in document["string_field"].lower(), ) - @pytest.mark.skipif( - not string_expr_opted_in("startswith"), - reason="startswith expr is disabled.", - ) - def test_startswith(self): - # WARNING: SQLite treats "startswith" as "istartswith"! - # You should avoid "startswith" unless you know what you are doing! - self._assert_query_match_predicate( - ["string_field", "startswith", "paper"], - lambda document: "string_field" in document - and document["string_field"] is not None - and document["string_field"].startswith("paper"), - ) - def test_istartswith(self): self._assert_query_match_predicate( ["string_field", "istartswith", "paper"], @@ -369,20 +323,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): and document["string_field"].lower().startswith("paper"), ) - @pytest.mark.skipif( - not string_expr_opted_in("endswith"), - reason="endswith expr is disabled.", - ) - def test_endswith(self): - # WARNING: SQLite treats "endswith" as "iendswith"! - # You should avoid "endswith" unless you know what you are doing! - self._assert_query_match_predicate( - ["string_field", "iendswith", "less"], - lambda document: "string_field" in document - and document["string_field"] is not None - and document["string_field"].lower().endswith("less"), - ) - def test_iendswith(self): self._assert_query_match_predicate( ["string_field", "iendswith", "less"], @@ -391,32 +331,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): and document["string_field"].lower().endswith("less"), ) - @pytest.mark.skipif( - not string_expr_opted_in("regex"), - reason="regex expr is disabled.", - ) - def test_regex(self): - # WARNING: the regex syntax is database dependent! - self._assert_query_match_predicate( - ["string_field", "regex", r"^p.+s$"], - lambda document: "string_field" in document - and document["string_field"] is not None - and re.match(r"^p.+s$", document["string_field"]), - ) - - @pytest.mark.skipif( - not string_expr_opted_in("iregex"), - reason="iregex expr is disabled.", - ) - def test_iregex(self): - # WARNING: the regex syntax is database dependent! - self._assert_query_match_predicate( - ["string_field", "iregex", r"^p.+s$"], - lambda document: "string_field" in document - and document["string_field"] is not None - and re.match(r"^p.+s$", document["string_field"], re.IGNORECASE), - ) - def test_url_field_istartswith(self): # URL fields supports all of the expressions above. # Just showing one of them here. @@ -427,28 +341,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): and document["url_field"].startswith("http://"), ) - @pytest.mark.skipif( - not string_expr_opted_in("iregex"), - reason="regex expr is disabled.", - ) - def test_monetary_field_iregex(self): - # Monetary fields supports all of the expressions above. - # Just showing one of them here. - # - # Unfortunately we can't do arithmetic comparisons on monetary field, - # but you are welcome to use regex to do some of that. - # E.g., USD between 100.00 and 999.99: - self._assert_query_match_predicate( - ["monetary_field", "regex", r"USD[1-9][0-9]{2}\.[0-9]{2}"], - lambda document: "monetary_field" in document - and document["monetary_field"] is not None - and re.match( - r"USD[1-9][0-9]{2}\.[0-9]{2}", - document["monetary_field"], - re.IGNORECASE, - ), - ) - # ==========================================================# # Arithmetic comparisons # # ==========================================================# @@ -502,6 +394,17 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): and document["date_field"].year >= 2024, ) + def test_gt_monetary(self): + self._assert_query_match_predicate( + ["monetary_field", "gt", "99"], + lambda document: "monetary_field" in document + and document["monetary_field"] is not None + and ( + document["monetary_field"] == "USD100.00" # With currency symbol + or document["monetary_field"] == "101.00" # No currency symbol + ), + ) + # ==========================================================# # Subset check (document link field only) # # ==========================================================# @@ -586,68 +489,57 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): def test_invalid_json(self): self._assert_validation_error( "not valid json", - ["custom_field_lookup"], + ["custom_field_query"], "must be valid JSON", ) def test_invalid_expression(self): self._assert_validation_error( json.dumps("valid json but not valid expr"), - ["custom_field_lookup"], - "Invalid custom field lookup expression", + ["custom_field_query"], + "Invalid custom field query expression", ) def test_invalid_custom_field_name(self): self._assert_validation_error( json.dumps(["invalid name", "iexact", "foo"]), - ["custom_field_lookup", "0"], + ["custom_field_query", "0"], "is not a valid custom field", ) def test_invalid_operator(self): self._assert_validation_error( json.dumps(["integer_field", "iexact", "foo"]), - ["custom_field_lookup", "1"], - "does not support lookup expr", + ["custom_field_query", "1"], + "does not support query expr", ) def test_invalid_value(self): self._assert_validation_error( json.dumps(["select_field", "exact", "not an option"]), - ["custom_field_lookup", "2"], + ["custom_field_query", "2"], "integer", ) def test_invalid_logical_operator(self): self._assert_validation_error( json.dumps(["invalid op", ["integer_field", "gt", 0]]), - ["custom_field_lookup", "0"], + ["custom_field_query", "0"], "Invalid logical operator", ) def test_invalid_expr_list(self): self._assert_validation_error( json.dumps(["AND", "not a list"]), - ["custom_field_lookup", "1"], + ["custom_field_query", "1"], "Invalid expression list", ) def test_invalid_operator_prefix(self): self._assert_validation_error( json.dumps(["integer_field", "foo__gt", 0]), - ["custom_field_lookup", "1"], - "does not support lookup expr", - ) - - @pytest.mark.skipif( - string_expr_opted_in("regex"), - reason="user opted into allowing regex expr", - ) - def test_disabled_operator(self): - self._assert_validation_error( - json.dumps(["string_field", "regex", r"^p.+s$"]), - ["custom_field_lookup", "1"], - "disabled by default", + ["custom_field_query", "1"], + "does not support query expr", ) def test_query_too_deep(self): @@ -656,7 +548,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): query = ["NOT", query] self._assert_validation_error( json.dumps(query), - ["custom_field_lookup", *(["1"] * 10)], + ["custom_field_query", *(["1"] * 10)], "Maximum nesting depth exceeded", ) @@ -665,6 +557,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): query = ["AND", [atom for _ in range(21)]] self._assert_validation_error( json.dumps(query), - ["custom_field_lookup", "1", "20"], + ["custom_field_query", "1", "20"], "Maximum number of query conditions exceeded", ) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 023e826c9..ab943f30f 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -1195,20 +1195,3 @@ EMAIL_ENABLE_GPG_DECRYPTOR: Final[bool] = __get_boolean( # Soft Delete # ############################################################################### EMPTY_TRASH_DELAY = max(__get_int("PAPERLESS_EMPTY_TRASH_DELAY", 30), 1) - -############################################################################### -# custom_field_lookup Filter Settings # -############################################################################### - -CUSTOM_FIELD_LOOKUP_OPT_IN = __get_list( - "PAPERLESS_CUSTOM_FIELD_LOOKUP_OPT_IN", - default=[], -) -CUSTOM_FIELD_LOOKUP_MAX_DEPTH = __get_int( - "PAPERLESS_CUSTOM_FIELD_LOOKUP_MAX_DEPTH", - default=10, -) -CUSTOM_FIELD_LOOKUP_MAX_ATOMS = __get_int( - "PAPERLESS_CUSTOM_FIELD_LOOKUP_MAX_ATOMS", - default=20, -) From a85f3bcbb7b692c7a22657c2a342b0165de94a12 Mon Sep 17 00:00:00 2001 From: Martin Richtarsky Date: Thu, 3 Oct 2024 05:21:35 +0200 Subject: [PATCH 35/74] Enhancement: check for mail destination directory, log post-consume errors (#7808) --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- src/paperless_mail/mail.py | 24 +++++-- src/paperless_mail/tests/test_mail.py | 92 +++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 84f97b742..77d293ea0 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -28,6 +28,7 @@ from imap_tools import MailboxFolderSelectError from imap_tools import MailBoxUnencrypted from imap_tools import MailMessage from imap_tools import MailMessageFlags +from imap_tools import errors from imap_tools.mailbox import MailBoxTls from imap_tools.query import LogicOperator @@ -266,7 +267,14 @@ def apply_mail_action( M.folder.set(rule.folder) action = get_rule_action(rule, supports_gmail_labels) - action.post_consume(M, message_uid, rule.action_parameter) + try: + action.post_consume(M, message_uid, rule.action_parameter) + except errors.ImapToolsError: + logger = logging.getLogger("paperless_mail") + logger.exception( + "Error while processing mail action during post_consume", + ) + raise ProcessedMail.objects.create( owner=rule.owner, @@ -570,13 +578,17 @@ class MailAccountHandler(LoggingMixin): rule: MailRule, supports_gmail_labels: bool, ): - self.log.debug(f"Rule {rule}: Selecting folder {rule.folder}") - + folders = [rule.folder] + # In case of MOVE, make sure also the destination exists + if rule.action == MailRule.MailAction.MOVE: + folders.insert(0, rule.action_parameter) try: - M.folder.set(rule.folder) + for folder in folders: + self.log.debug(f"Rule {rule}: Selecting folder {folder}") + M.folder.set(folder) except MailboxFolderSelectError as err: self.log.error( - f"Unable to access folder {rule.folder}, attempting folder listing", + f"Unable to access folder {folder}, attempting folder listing", ) try: for folder_info in M.folder.list(): @@ -588,7 +600,7 @@ class MailAccountHandler(LoggingMixin): ) raise MailError( - f"Rule {rule}: Folder {rule.folder} " + f"Rule {rule}: Folder {folder} " f"does not exist in account {rule.account}", ) from err diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 9078335a6..b1e3ff06e 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -10,6 +10,7 @@ import pytest from django.core.management import call_command from django.db import DatabaseError from django.test import TestCase +from django.utils import timezone from imap_tools import NOT from imap_tools import EmailAddress from imap_tools import FolderInfo @@ -17,6 +18,7 @@ from imap_tools import MailboxFolderSelectError from imap_tools import MailboxLoginError from imap_tools import MailMessage from imap_tools import MailMessageFlags +from imap_tools import errors from documents.models import Correspondent from documents.tests.utils import DirectoriesMixin @@ -28,6 +30,7 @@ from paperless_mail.mail import TagMailAction from paperless_mail.mail import apply_mail_action from paperless_mail.models import MailAccount from paperless_mail.models import MailRule +from paperless_mail.models import ProcessedMail @dataclasses.dataclass @@ -1424,6 +1427,95 @@ class TestMail( ) # still 2 +class TestPostConsumeAction(TestCase): + def setUp(self): + self.account = MailAccount.objects.create( + name="test", + imap_server="imap.test.com", + imap_port=993, + imap_security=MailAccount.ImapSecurity.SSL, + username="testuser", + password="password", + ) + self.rule = MailRule.objects.create( + name="testrule", + account=self.account, + action=MailRule.MailAction.MARK_READ, + action_parameter="", + folder="INBOX", + ) + self.message_uid = "12345" + self.message_subject = "Test Subject" + self.message_date = timezone.make_aware(timezone.datetime(2023, 1, 1, 12, 0, 0)) + + @mock.patch("paperless_mail.mail.get_mailbox") + @mock.patch("paperless_mail.mail.mailbox_login") + @mock.patch("paperless_mail.mail.get_rule_action") + def test_post_consume_success( + self, + mock_get_rule_action, + mock_mailbox_login, + mock_get_mailbox, + ): + mock_mailbox = mock.MagicMock() + mock_get_mailbox.return_value.__enter__.return_value = mock_mailbox + mock_action = mock.MagicMock() + mock_get_rule_action.return_value = mock_action + + apply_mail_action( + result=[], + rule_id=self.rule.pk, + message_uid=self.message_uid, + message_subject=self.message_subject, + message_date=self.message_date, + ) + + mock_mailbox_login.assert_called_once_with(mock_mailbox, self.account) + mock_mailbox.folder.set.assert_called_once_with(self.rule.folder) + mock_action.post_consume.assert_called_once_with( + mock_mailbox, + self.message_uid, + self.rule.action_parameter, + ) + + processed_mail = ProcessedMail.objects.get(uid=self.message_uid) + self.assertEqual(processed_mail.status, "SUCCESS") + + @mock.patch("paperless_mail.mail.get_mailbox") + @mock.patch("paperless_mail.mail.mailbox_login") + @mock.patch("paperless_mail.mail.get_rule_action") + def test_post_consume_failure( + self, + mock_get_rule_action, + mock_mailbox_login, + mock_get_mailbox, + ): + mock_mailbox = mock.MagicMock() + mock_get_mailbox.return_value.__enter__.return_value = mock_mailbox + mock_action = mock.MagicMock() + mock_get_rule_action.return_value = mock_action + mock_action.post_consume.side_effect = errors.ImapToolsError("Test Exception") + + with ( + self.assertRaises(errors.ImapToolsError), + self.assertLogs("paperless.mail", level="ERROR") as cm, + ): + apply_mail_action( + result=[], + rule_id=self.rule.pk, + message_uid=self.message_uid, + message_subject=self.message_subject, + message_date=self.message_date, + ) + error_str = cm.output[0] + expected_str = "Error while processing mail action during post_consume" + self.assertIn(expected_str, error_str) + + processed_mail = ProcessedMail.objects.get(uid=self.message_uid) + self.assertEqual(processed_mail.status, "FAILED") + self.assertIn("Test Exception", processed_mail.error) + + class TestManagementCommand(TestCase): @mock.patch( "paperless_mail.management.commands.mail_fetcher.tasks.process_mail_accounts", From 79471c59eba5f74943a17f345421ecd1a8b72fcd Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:21:47 -0700 Subject: [PATCH 36/74] Add missing interface to resolve test warning --- .../custom-fields-query-dropdown.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts index 923907158..011ae1bc1 100644 --- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts @@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, + OnDestroy, Output, ViewChild, } from '@angular/core' @@ -148,7 +149,7 @@ export class CustomFieldQueriesModel { templateUrl: './custom-fields-query-dropdown.component.html', styleUrls: ['./custom-fields-query-dropdown.component.scss'], }) -export class CustomFieldsQueryDropdownComponent { +export class CustomFieldsQueryDropdownComponent implements OnDestroy { public CustomFieldQueryComponentType = CustomFieldQueryElementType public CustomFieldQueryOperator = CustomFieldQueryOperator public CustomFieldDataType = CustomFieldDataType From 3507d297fa7d2fc589634404d92e8cc02103a854 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:00:28 -0700 Subject: [PATCH 37/74] Enhancement: management list button improvements (#7848) --- src-ui/messages.xlf | 126 ++++++++++-------- .../custom-fields.component.html | 23 +++- .../custom-fields.component.scss | 4 + .../custom-fields.component.spec.ts | 24 +++- .../custom-fields/custom-fields.component.ts | 21 ++- .../management-list.component.html | 16 ++- .../management-list.component.spec.ts | 9 +- src-ui/src/app/data/custom-field.ts | 1 + src/documents/serialisers.py | 3 + src/documents/tests/test_api_custom_fields.py | 49 +++++++ src/documents/views.py | 26 ++++ 11 files changed, 236 insertions(+), 66 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 3fffe4f6e..3570a77c1 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -322,20 +322,24 @@ 128 - src/app/components/manage/management-list/management-list.component.html - 90 + src/app/components/manage/custom-fields/custom-fields.component.html + 54 src/app/components/manage/management-list/management-list.component.html - 90 + 101 src/app/components/manage/management-list/management-list.component.html - 90 + 101 src/app/components/manage/management-list/management-list.component.html - 90 + 101 + + + src/app/components/manage/management-list/management-list.component.html + 101 @@ -1493,7 +1497,11 @@ src/app/components/manage/custom-fields/custom-fields.component.html - 34 + 36 + + + src/app/components/manage/custom-fields/custom-fields.component.html + 48 src/app/components/manage/mail/mail.component.html @@ -1529,35 +1537,35 @@ src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 84 + 83 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.html - 96 + 95 src/app/components/manage/management-list/management-list.component.ts @@ -2219,7 +2227,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 73 + 80 src/app/components/manage/mail/mail.component.ts @@ -2418,7 +2426,11 @@ src/app/components/manage/custom-fields/custom-fields.component.html - 31 + 35 + + + src/app/components/manage/custom-fields/custom-fields.component.html + 45 src/app/components/manage/mail/mail.component.html @@ -2438,35 +2450,35 @@ src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 83 + 82 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/management-list/management-list.component.html - 93 + 92 src/app/components/manage/workflows/workflows.component.html @@ -2570,7 +2582,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 75 + 82 src/app/components/manage/mail/mail.component.ts @@ -3286,7 +3298,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 56 + 63 @@ -3297,7 +3309,7 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 63 + 70 @@ -7475,39 +7487,62 @@ 18 + + Filter Documents () + + src/app/components/manage/custom-fields/custom-fields.component.html + 38 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + + src/app/components/manage/management-list/management-list.component.html + 85 + + No fields defined. src/app/components/manage/custom-fields/custom-fields.component.html - 42 + 63 Confirm delete field src/app/components/manage/custom-fields/custom-fields.component.ts - 71 + 78 This operation will permanently delete this field. src/app/components/manage/custom-fields/custom-fields.component.ts - 72 + 79 Deleted field src/app/components/manage/custom-fields/custom-fields.component.ts - 81 + 88 Error deleting field. src/app/components/manage/custom-fields/custom-fields.component.ts - 86 + 93 @@ -7799,42 +7834,23 @@ 39 - - Filter Documents - - src/app/components/manage/management-list/management-list.component.html - 82 - - - src/app/components/manage/management-list/management-list.component.html - 82 - - - src/app/components/manage/management-list/management-list.component.html - 82 - - - src/app/components/manage/management-list/management-list.component.html - 82 - - {VAR_PLURAL, plural, =1 {One } other { total }} src/app/components/manage/management-list/management-list.component.html - 110 + 116 src/app/components/manage/management-list/management-list.component.html - 110 + 116 src/app/components/manage/management-list/management-list.component.html - 110 + 116 src/app/components/manage/management-list/management-list.component.html - 110 + 116 diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html index c87a93050..8439cd1a7 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html @@ -26,7 +26,21 @@
    {{getDataType(field)}}
    -
    +
    +
    + +
    + + + @if (field.document_count > 0) { + + } +
    +
    +
    +
    @@ -34,6 +48,13 @@  Delete
    + @if (field.document_count > 0) { +
    + +
    + }
    diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss index e69de29bb..dfdd20433 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss @@ -0,0 +1,4 @@ +// hide caret on mobile dropdown +.d-block.d-sm-none .dropdown-toggle::after { + display: none; +} diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts index 2bb6c82d7..5feb17055 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts @@ -22,6 +22,12 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type' +import { + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from 'src/app/data/custom-field-query' const fields: CustomField[] = [ { @@ -42,6 +48,7 @@ describe('CustomFieldsComponent', () => { let customFieldsService: CustomFieldsService let modalService: NgbModal let toastService: ToastService + let listViewService: DocumentListViewService beforeEach(() => { TestBed.configureTestingModule({ @@ -83,6 +90,7 @@ describe('CustomFieldsComponent', () => { ) modalService = TestBed.inject(NgbModal) toastService = TestBed.inject(ToastService) + listViewService = TestBed.inject(DocumentListViewService) fixture = TestBed.createComponent(CustomFieldsComponent) component = fixture.componentInstance @@ -145,7 +153,7 @@ describe('CustomFieldsComponent', () => { const deleteSpy = jest.spyOn(customFieldsService, 'delete') const reloadSpy = jest.spyOn(component, 'reload') - const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4] + const deleteButton = fixture.debugElement.queryAll(By.css('button'))[5] deleteButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -162,4 +170,18 @@ describe('CustomFieldsComponent', () => { editDialog.confirmClicked.emit() expect(reloadSpy).toHaveBeenCalled() }) + + it('should support filter documents', () => { + const filterSpy = jest.spyOn(listViewService, 'quickFilter') + component.filterDocuments(fields[0]) + expect(filterSpy).toHaveBeenCalledWith([ + { + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify([ + CustomFieldQueryLogicalOperator.Or, + [[fields[0].id, CustomFieldQueryOperator.Exists, true]], + ]), + }, + ]) + }) }) diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts index 33f2751a9..60bbcc09c 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts @@ -9,6 +9,12 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' +import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type' +import { + CustomFieldQueryLogicalOperator, + CustomFieldQueryOperator, +} from 'src/app/data/custom-field-query' @Component({ selector: 'pngx-custom-fields', @@ -26,7 +32,8 @@ export class CustomFieldsComponent private customFieldsService: CustomFieldsService, public permissionsService: PermissionsService, private modalService: NgbModal, - private toastService: ToastService + private toastService: ToastService, + private documentListViewService: DocumentListViewService ) { super() } @@ -92,4 +99,16 @@ export class CustomFieldsComponent getDataType(field: CustomField): string { return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name } + + filterDocuments(field: CustomField) { + this.documentListViewService.quickFilter([ + { + rule_type: FILTER_CUSTOM_FIELDS_QUERY, + value: JSON.stringify([ + CustomFieldQueryLogicalOperator.Or, + [[field.id, CustomFieldQueryOperator.Exists, true]], + ]), + }, + ]) + } } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.html b/src-ui/src/app/components/manage/management-list/management-list.component.html index a180f4941..e9a181819 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.html +++ b/src-ui/src/app/components/manage/management-list/management-list.component.html @@ -79,16 +79,15 @@
    - + @if (object.document_count > 0) { + + }
    -
    - +
    @@ -96,6 +95,13 @@  Delete
    + @if (object.document_count > 0) { +
    + +
    + }
    Matching Document count{{column.name}}{{column.name}}Actions
    {{ getMatching(object) }} {{ object.document_count }} + @if (column.rendersHtml) {
    } @else { diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index 84cee9ddc..27165a8fb 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -44,6 +44,8 @@ export interface ManagementListColumn { valueFn: any rendersHtml?: boolean + + hideOnMobile?: boolean } @Directive() diff --git a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.spec.ts b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.spec.ts index 0816dae7d..00cb2b037 100644 --- a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.spec.ts +++ b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.spec.ts @@ -11,6 +11,8 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon import { StoragePathListComponent } from './storage-path-list.component' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' +import { StoragePath } from 'src/app/data/storage-path' describe('StoragePathListComponent', () => { let component: StoragePathListComponent @@ -24,6 +26,7 @@ describe('StoragePathListComponent', () => { SortableDirective, PageHeaderComponent, IfPermissionsDirective, + SafeHtmlPipe, ], imports: [ NgbPaginationModule, @@ -71,4 +74,15 @@ describe('StoragePathListComponent', () => { 'Do you really want to delete the storage path "StoragePath1"?' ) }) + + it('should truncate path if necessary', () => { + const path: StoragePath = { + id: 1, + name: 'StoragePath1', + path: 'a'.repeat(100), + } + expect(component.extraColumns[0].valueFn(path)).toEqual( + `${'a'.repeat(49)}...` + ) + }) }) diff --git a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts index d227f01a5..66819284d 100644 --- a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts +++ b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts @@ -40,8 +40,10 @@ export class StoragePathListComponent extends ManagementListComponent { - return c.path + return `${c.path?.slice(0, 49)}${c.path?.length > 50 ? '...' : ''}` }, }, ] diff --git a/src/documents/checks.py b/src/documents/checks.py index 69027bf21..a97c517aa 100644 --- a/src/documents/checks.py +++ b/src/documents/checks.py @@ -2,12 +2,14 @@ import textwrap from django.conf import settings from django.core.checks import Error +from django.core.checks import Warning from django.core.checks import register from django.core.exceptions import FieldError from django.db.utils import OperationalError from django.db.utils import ProgrammingError from documents.signals import document_consumer_declaration +from documents.templating.utils import convert_format_str_to_template_format @register() @@ -69,3 +71,19 @@ def parser_check(app_configs, **kwargs): ] else: return [] + + +@register() +def filename_format_check(app_configs, **kwargs): + if settings.FILENAME_FORMAT: + converted_format = convert_format_str_to_template_format( + settings.FILENAME_FORMAT, + ) + if converted_format != settings.FILENAME_FORMAT: + return [ + Warning( + f"Filename format {settings.FILENAME_FORMAT} is using the old style, please update to use double curly brackets", + hint=converted_format, + ), + ] + return [] diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index 700a16d8b..6d02bf684 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -1,21 +1,10 @@ -import logging import os -from collections import defaultdict -from pathlib import PurePath -import pathvalidate from django.conf import settings -from django.template.defaultfilters import slugify -from django.utils import timezone from documents.models import Document - -logger = logging.getLogger("paperless.filehandling") - - -class defaultdictNoStr(defaultdict): - def __str__(self): - raise ValueError("Don't use {tags} directly.") +from documents.templating.filepath import validate_filepath_template_and_render +from documents.templating.utils import convert_format_str_to_template_format def create_source_path_directory(source_path): @@ -54,32 +43,6 @@ def delete_empty_directories(directory, root): directory = os.path.normpath(os.path.dirname(directory)) -def many_to_dictionary(field): - # Converts ManyToManyField to dictionary by assuming, that field - # entries contain an _ or - which will be used as a delimiter - mydictionary = dict() - - for index, t in enumerate(field.all()): - # Populate tag names by index - mydictionary[index] = slugify(t.name) - - # Find delimiter - delimiter = t.name.find("_") - - if delimiter == -1: - delimiter = t.name.find("-") - - if delimiter == -1: - continue - - key = t.name[:delimiter] - value = t.name[delimiter + 1 :] - - mydictionary[slugify(key)] = slugify(value) - - return mydictionary - - def generate_unique_filename(doc, archive_filename=False): """ Generates a unique filename for doc in settings.ORIGINALS_DIR. @@ -134,116 +97,51 @@ def generate_filename( archive_filename=False, ): path = "" - filename_format = settings.FILENAME_FORMAT - try: - if doc.storage_path is not None: - logger.debug( - f"Document has storage_path {doc.storage_path.id} " - f"({doc.storage_path.path}) set", - ) - filename_format = doc.storage_path.path - - if filename_format is not None: - tags = defaultdictNoStr( - lambda: slugify(None), - many_to_dictionary(doc.tags), - ) - - tag_list = pathvalidate.sanitize_filename( - ",".join( - sorted(tag.name for tag in doc.tags.all()), - ), - replacement_text="-", - ) - - no_value_default = "-none-" - - if doc.correspondent: - correspondent = pathvalidate.sanitize_filename( - doc.correspondent.name, - replacement_text="-", - ) - else: - correspondent = no_value_default - - if doc.document_type: - document_type = pathvalidate.sanitize_filename( - doc.document_type.name, - replacement_text="-", - ) - else: - document_type = no_value_default - - if doc.archive_serial_number: - asn = str(doc.archive_serial_number) - else: - asn = no_value_default - - if doc.owner is not None: - owner_username_str = str(doc.owner.username) - else: - owner_username_str = no_value_default - - if doc.original_filename is not None: - # No extension - original_name = PurePath(doc.original_filename).with_suffix("").name - else: - original_name = no_value_default - - # Convert UTC database datetime to localized date - local_added = timezone.localdate(doc.added) - local_created = timezone.localdate(doc.created) - - path = filename_format.format( - title=pathvalidate.sanitize_filename(doc.title, replacement_text="-"), - correspondent=correspondent, - document_type=document_type, - created=local_created.isoformat(), - created_year=local_created.strftime("%Y"), - created_year_short=local_created.strftime("%y"), - created_month=local_created.strftime("%m"), - created_month_name=local_created.strftime("%B"), - created_month_name_short=local_created.strftime("%b"), - created_day=local_created.strftime("%d"), - added=local_added.isoformat(), - added_year=local_added.strftime("%Y"), - added_year_short=local_added.strftime("%y"), - added_month=local_added.strftime("%m"), - added_month_name=local_added.strftime("%B"), - added_month_name_short=local_added.strftime("%b"), - added_day=local_added.strftime("%d"), - asn=asn, - tags=tags, - tag_list=tag_list, - owner_username=owner_username_str, - original_name=original_name, - doc_pk=f"{doc.pk:07}", - ).strip() - - if settings.FILENAME_FORMAT_REMOVE_NONE: - path = path.replace("/-none-/", "/") # remove empty directories - path = path.replace(" -none-", "") # remove when spaced, with space - path = path.replace("-none-", "") # remove rest of the occurrences - - path = path.replace("-none-", "none") # backward compatibility - path = path.strip(os.sep) - - except (ValueError, KeyError, IndexError): - logger.warning( - f"Invalid filename_format '{filename_format}', falling back to default", + def format_filename(document: Document, template_str: str) -> str | None: + rendered_filename = validate_filepath_template_and_render( + template_str, + document, ) + if rendered_filename is None: + return None + + # Apply this setting. It could become a filter in the future (or users could use |default) + if settings.FILENAME_FORMAT_REMOVE_NONE: + rendered_filename = rendered_filename.replace("/-none-/", "/") + rendered_filename = rendered_filename.replace(" -none-", "") + rendered_filename = rendered_filename.replace("-none-", "") + + rendered_filename = rendered_filename.replace( + "-none-", + "none", + ) # backward compatibility + + return rendered_filename + + # Determine the source of the format string + if doc.storage_path is not None: + filename_format = doc.storage_path.path + elif settings.FILENAME_FORMAT is not None: + # Maybe convert old to new style + filename_format = convert_format_str_to_template_format( + settings.FILENAME_FORMAT, + ) + else: + filename_format = None + + # If we have one, render it + if filename_format is not None: + path = format_filename(doc, filename_format) counter_str = f"_{counter:02}" if counter else "" - filetype_str = ".pdf" if archive_filename else doc.file_type - if len(path) > 0: + if path: filename = f"{path}{counter_str}{filetype_str}" else: filename = f"{doc.pk:07}{counter_str}{filetype_str}" - # Append .gpg for encrypted files if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG: filename += ".gpg" diff --git a/src/documents/migrations/1012_fix_archive_files.py b/src/documents/migrations/1012_fix_archive_files.py index 87d6ddc78..1d12c439b 100644 --- a/src/documents/migrations/1012_fix_archive_files.py +++ b/src/documents/migrations/1012_fix_archive_files.py @@ -4,6 +4,7 @@ import hashlib import logging import os import shutil +from collections import defaultdict from time import sleep import pathvalidate @@ -12,14 +13,41 @@ from django.db import migrations from django.db import models from django.template.defaultfilters import slugify -from documents.file_handling import defaultdictNoStr -from documents.file_handling import many_to_dictionary - logger = logging.getLogger("paperless.migrations") + ############################################################################### # This is code copied straight paperless before the change. ############################################################################### +class defaultdictNoStr(defaultdict): + def __str__(self): # pragma: no cover + raise ValueError("Don't use {tags} directly.") + + +def many_to_dictionary(field): # pragma: no cover + # Converts ManyToManyField to dictionary by assuming, that field + # entries contain an _ or - which will be used as a delimiter + mydictionary = dict() + + for index, t in enumerate(field.all()): + # Populate tag names by index + mydictionary[index] = slugify(t.name) + + # Find delimiter + delimiter = t.name.find("_") + + if delimiter == -1: + delimiter = t.name.find("-") + + if delimiter == -1: + continue + + key = t.name[:delimiter] + value = t.name[delimiter + 1 :] + + mydictionary[slugify(key)] = slugify(value) + + return mydictionary def archive_name_from_filename(filename): diff --git a/src/documents/migrations/1055_alter_storagepath_path.py b/src/documents/migrations/1055_alter_storagepath_path.py new file mode 100644 index 000000000..8231aacd7 --- /dev/null +++ b/src/documents/migrations/1055_alter_storagepath_path.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.1 on 2024-10-03 14:47 + +from django.conf import settings +from django.db import migrations +from django.db import models +from django.db import transaction +from filelock import FileLock + +from documents.templating.utils import convert_format_str_to_template_format + + +def convert_from_format_to_template(apps, schema_editor): + StoragePath = apps.get_model("documents", "StoragePath") + + with transaction.atomic(), FileLock(settings.MEDIA_LOCK): + for storage_path in StoragePath.objects.all(): + storage_path.path = convert_format_str_to_template_format(storage_path.path) + storage_path.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1054_customfieldinstance_value_monetary_amount_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="storagepath", + name="path", + field=models.CharField(max_length=2048, verbose_name="path"), + ), + migrations.RunPython( + convert_from_format_to_template, + migrations.RunPython.noop, + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 80476bffa..23325739c 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -127,7 +127,7 @@ class DocumentType(MatchingModel): class StoragePath(MatchingModel): path = models.CharField( _("path"), - max_length=512, + max_length=2048, ) class Meta(MatchingModel.Meta): diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index f326b4eee..7c6e5a3ff 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1,4 +1,5 @@ import datetime +import logging import math import re import zoneinfo @@ -52,8 +53,12 @@ from documents.models import WorkflowTrigger from documents.parsers import is_mime_type_supported from documents.permissions import get_groups_with_only_permission from documents.permissions import set_permissions_for_object +from documents.templating.filepath import validate_filepath_template_and_render +from documents.templating.utils import convert_format_str_to_template_format from documents.validators import uri_validator +logger = logging.getLogger("paperless.serializers") + # https://www.django-rest-framework.org/api-guide/serializers/#example class DynamicFieldsModelSerializer(serializers.ModelSerializer): @@ -1482,38 +1487,18 @@ class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer): "set_permissions", ) - def validate_path(self, path): - try: - path.format( - title="title", - correspondent="correspondent", - document_type="document_type", - created="created", - created_year="created_year", - created_year_short="created_year_short", - created_month="created_month", - created_month_name="created_month_name", - created_month_name_short="created_month_name_short", - created_day="created_day", - added="added", - added_year="added_year", - added_year_short="added_year_short", - added_month="added_month", - added_month_name="added_month_name", - added_month_name_short="added_month_name_short", - added_day="added_day", - asn="asn", - tags="tags", - tag_list="tag_list", - owner_username="someone", - original_name="testfile", - doc_pk="doc_pk", + def validate_path(self, path: str): + converted_path = convert_format_str_to_template_format(path) + if converted_path != path: + logger.warning( + f"Storage path {path} is not using the new style format, consider updating", ) + result = validate_filepath_template_and_render(converted_path) - except KeyError as err: - raise serializers.ValidationError(_("Invalid variable detected.")) from err + if result is None: + raise serializers.ValidationError(_("Invalid variable detected.")) - return path + return converted_path def update(self, instance, validated_data): """ diff --git a/src/documents/templating/__init__.py b/src/documents/templating/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py new file mode 100644 index 000000000..ec902bf54 --- /dev/null +++ b/src/documents/templating/filepath.py @@ -0,0 +1,333 @@ +import logging +import os +import re +from collections.abc import Iterable +from datetime import datetime +from pathlib import PurePath + +import pathvalidate +from django.utils import timezone +from django.utils.dateparse import parse_date +from jinja2 import StrictUndefined +from jinja2 import Template +from jinja2 import TemplateSyntaxError +from jinja2 import UndefinedError +from jinja2 import make_logging_undefined +from jinja2.sandbox import SandboxedEnvironment +from jinja2.sandbox import SecurityError + +from documents.models import Correspondent +from documents.models import CustomField +from documents.models import CustomFieldInstance +from documents.models import Document +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag + +logger = logging.getLogger("paperless.templating") + +_LogStrictUndefined = make_logging_undefined(logger, StrictUndefined) + + +class FilePathEnvironment(SandboxedEnvironment): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.undefined_tracker = None + + def is_safe_callable(self, obj): + # Block access to .save() and .delete() methods + if callable(obj) and getattr(obj, "__name__", None) in ( + "save", + "delete", + "update", + ): + return False + # Call the parent method for other cases + return super().is_safe_callable(obj) + + +_template_environment = FilePathEnvironment( + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=False, + autoescape=False, + extensions=["jinja2.ext.loopcontrols"], + undefined=_LogStrictUndefined, +) + + +class FilePathTemplate(Template): + def render(self, *args, **kwargs) -> str: + def clean_filepath(value: str) -> str: + """ + Clean up a filepath by: + 1. Removing newlines and carriage returns + 2. Removing extra spaces before and after forward slashes + 3. Preserving spaces in other parts of the path + """ + value = value.replace("\n", "").replace("\r", "") + value = re.sub(r"\s*/\s*", "/", value) + + # We remove trailing and leading separators, as these are always relative paths, not absolute, even if the user + # tries + return value.strip().strip(os.sep) + + original_render = super().render(*args, **kwargs) + + return clean_filepath(original_render) + + +def get_cf_value( + custom_field_data: dict[str, dict[str, str]], + name: str, + default: str | None = None, +) -> str | None: + if name in custom_field_data: + return custom_field_data[name]["value"] + elif default is not None: + return default + return None + + +_template_environment.filters["get_cf_value"] = get_cf_value + + +def format_datetime(value: str | datetime, format: str) -> str: + if isinstance(value, str): + value = parse_date(value) + return value.strftime(format=format) + + +_template_environment.filters["datetime"] = format_datetime + + +def create_dummy_document(): + """ + Create a dummy Document instance with all possible fields filled + """ + # Populate the document with representative values for every field + dummy_doc = Document( + pk=1, + title="Sample Title", + correspondent=Correspondent(name="Sample Correspondent"), + storage_path=StoragePath(path="/dummy/path"), + document_type=DocumentType(name="Sample Type"), + content="This is some sample document content.", + mime_type="application/pdf", + checksum="dummychecksum12345678901234567890123456789012", + archive_checksum="dummyarchivechecksum123456789012345678901234", + page_count=5, + created=timezone.now(), + modified=timezone.now(), + storage_type=Document.STORAGE_TYPE_UNENCRYPTED, + added=timezone.now(), + filename="/dummy/filename.pdf", + archive_filename="/dummy/archive_filename.pdf", + original_filename="original_file.pdf", + archive_serial_number=12345, + ) + return dummy_doc + + +def get_creation_date_context(document: Document) -> dict[str, str]: + """ + Given a Document, localizes the creation date and builds a context dictionary with some common, shorthand + formatted values from it + """ + local_created = timezone.localdate(document.created) + + return { + "created": local_created.isoformat(), + "created_year": local_created.strftime("%Y"), + "created_year_short": local_created.strftime("%y"), + "created_month": local_created.strftime("%m"), + "created_month_name": local_created.strftime("%B"), + "created_month_name_short": local_created.strftime("%b"), + "created_day": local_created.strftime("%d"), + } + + +def get_added_date_context(document: Document) -> dict[str, str]: + """ + Given a Document, localizes the added date and builds a context dictionary with some common, shorthand + formatted values from it + """ + local_added = timezone.localdate(document.added) + + return { + "added": local_added.isoformat(), + "added_year": local_added.strftime("%Y"), + "added_year_short": local_added.strftime("%y"), + "added_month": local_added.strftime("%m"), + "added_month_name": local_added.strftime("%B"), + "added_month_name_short": local_added.strftime("%b"), + "added_day": local_added.strftime("%d"), + } + + +def get_basic_metadata_context( + document: Document, + *, + no_value_default: str, +) -> dict[str, str]: + """ + Given a Document, constructs some basic information about it. If certain values are not set, + they will be replaced with the no_value_default. + + Regardless of set or not, the values will be sanitized + """ + return { + "title": pathvalidate.sanitize_filename( + document.title, + replacement_text="-", + ), + "correspondent": pathvalidate.sanitize_filename( + document.correspondent.name, + replacement_text="-", + ) + if document.correspondent + else no_value_default, + "document_type": pathvalidate.sanitize_filename( + document.document_type.name, + replacement_text="-", + ) + if document.document_type + else no_value_default, + "asn": str(document.archive_serial_number) + if document.archive_serial_number + else no_value_default, + "owner_username": document.owner.username + if document.owner + else no_value_default, + "original_name": PurePath(document.original_filename).with_suffix("").name + if document.original_filename + else no_value_default, + "doc_pk": f"{document.pk:07}", + } + + +def get_tags_context(tags: Iterable[Tag]) -> dict[str, str | list[str]]: + """ + Given an Iterable of tags, constructs some context from them for usage + """ + return { + "tag_list": pathvalidate.sanitize_filename( + ",".join( + sorted(tag.name for tag in tags), + ), + replacement_text="-", + ), + # Assumed to be ordered, but a template could loop through to find what they want + "tag_name_list": [x.name for x in tags], + } + + +def get_custom_fields_context( + custom_fields: Iterable[CustomFieldInstance], +) -> dict[str, dict[str, dict[str, str]]]: + """ + Given an Iterable of CustomFieldInstance, builds a dictionary mapping the field name + to its type and value + """ + field_data = {"custom_fields": {}} + for field_instance in custom_fields: + type_ = pathvalidate.sanitize_filename( + field_instance.field.data_type, + replacement_text="-", + ) + # String types need to be sanitized + if field_instance.field.data_type in { + CustomField.FieldDataType.DOCUMENTLINK, + CustomField.FieldDataType.MONETARY, + CustomField.FieldDataType.STRING, + CustomField.FieldDataType.URL, + }: + value = pathvalidate.sanitize_filename( + field_instance.value, + replacement_text="-", + ) + elif ( + field_instance.field.data_type == CustomField.FieldDataType.SELECT + and field_instance.field.extra_data["select_options"] is not None + ): + options = field_instance.field.extra_data["select_options"] + value = pathvalidate.sanitize_filename( + options[int(field_instance.value)], + replacement_text="-", + ) + else: + value = field_instance.value + field_data["custom_fields"][ + pathvalidate.sanitize_filename( + field_instance.field.name, + replacement_text="-", + ) + ] = { + "type": type_, + "value": value, + } + return field_data + + +def validate_filepath_template_and_render( + template_string: str, + document: Document | None = None, +) -> str | None: + """ + Renders the given template string using either the given Document or using a dummy Document and data + + Returns None if the string is not valid or an error occurred, otherwise + """ + + # Create the dummy document object with all fields filled in for validation purposes + if document is None: + document = create_dummy_document() + tags_list = [Tag(name="Test Tag 1"), Tag(name="Another Test Tag")] + custom_fields = [ + CustomFieldInstance( + field=CustomField( + name="Text Custom Field", + data_type=CustomField.FieldDataType.STRING, + ), + value_text="Some String Text", + ), + ] + else: + # or use the real document information + tags_list = document.tags.order_by("name").all() + custom_fields = document.custom_fields.all() + + # Build the context dictionary + context = ( + {"document": document} + | get_basic_metadata_context(document, no_value_default="-none-") + | get_creation_date_context(document) + | get_added_date_context(document) + | get_tags_context(tags_list) + | get_custom_fields_context(custom_fields) + ) + + # Try rendering the template + try: + # We load the custom tag used to remove spaces and newlines from the final string around the user string + template = _template_environment.from_string( + template_string, + template_class=FilePathTemplate, + ) + rendered_template = template.render(context) + + # We're good! + return rendered_template + except UndefinedError: + # The undefined class logs this already for us + pass + except TemplateSyntaxError as e: + logger.warning(f"Template syntax error in filename generation: {e}") + except SecurityError as e: + logger.warning(f"Template attempted restricted operation: {e}") + except Exception as e: + logger.warning(f"Unknown error in filename generation: {e}") + logger.warning( + f"Invalid filename_format '{template_string}', falling back to default", + ) + return None diff --git a/src/documents/templating/utils.py b/src/documents/templating/utils.py new file mode 100644 index 000000000..894fda0f4 --- /dev/null +++ b/src/documents/templating/utils.py @@ -0,0 +1,24 @@ +import re + + +def convert_format_str_to_template_format(old_format: str) -> str: + """ + Converts old Python string format (with {}) to Jinja2 template style (with {{ }}), + while ignoring existing {{ ... }} placeholders. + + :param old_format: The old style format string (e.g., "{title} by {author}") + :return: Converted string in Django Template style (e.g., "{{ title }} by {{ author }}") + """ + + # Step 1: Match placeholders with single curly braces but not those with double braces + pattern = r"(?= 0 and document.archive_serial_number <= 200 %} + asn-000-200/{{title}} + {% elif document.archive_serial_number >= 201 and document.archive_serial_number <= 400 %} + asn-201-400 + {% if document.archive_serial_number >= 201 and document.archive_serial_number < 300 %} + /asn-2xx + {% elif document.archive_serial_number >= 300 and document.archive_serial_number < 400 %} + /asn-3xx + {% endif %} + {% endif %} + /{{ title }} + """ + sp.save() + self.assertEqual( + generate_filename(doc_a), + "somepath/asn-000-200/Does Matter/Does Matter.pdf", + ) + doc_a.archive_serial_number = 301 + doc_a.save() + self.assertEqual( + generate_filename(doc_a), + "somepath/asn-201-400/asn-3xx/Does Matter.pdf", + ) + + @override_settings( + FILENAME_FORMAT="{{creation_date}}/{{ title_name_str }}", + ) + def test_template_with_undefined_var(self): + """ + GIVEN: + - Filename format with one or more undefined variables + WHEN: + - Filepath for a document with this format is called + THEN: + - The first undefined variable is logged + - The default format is used + """ + doc_a = Document.objects.create( + title="Does Matter", + created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), + mime_type="application/pdf", + pk=2, + checksum="2", + archive_serial_number=25, + ) + + with self.assertLogs(level=logging.WARNING) as capture: + self.assertEqual( + generate_filename(doc_a), + "0000002.pdf", + ) + + self.assertEqual(len(capture.output), 1) + self.assertEqual( + capture.output[0], + "WARNING:paperless.templating:Template variable warning: 'creation_date' is undefined", + ) + + @override_settings( + FILENAME_FORMAT="{{created}}/{{ document.save() }}", + ) + def test_template_with_security(self): + """ + GIVEN: + - Filename format with one or more undefined variables + WHEN: + - Filepath for a document with this format is called + THEN: + - The first undefined variable is logged + - The default format is used + """ + doc_a = Document.objects.create( + title="Does Matter", + created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), + mime_type="application/pdf", + pk=2, + checksum="2", + archive_serial_number=25, + ) + + with self.assertLogs(level=logging.WARNING) as capture: + self.assertEqual( + generate_filename(doc_a), + "0000002.pdf", + ) + + self.assertEqual(len(capture.output), 1) + self.assertEqual( + capture.output[0], + "WARNING:paperless.templating:Template attempted restricted operation: > is not safely callable", + ) + + def test_template_with_custom_fields(self): + """ + GIVEN: + - Filename format which accesses custom field data + WHEN: + - Filepath for a document with this format is called + THEN: + - The custom field data is rendered + - If the field name is not defined, the default value is rendered, if any + """ + doc_a = Document.objects.create( + title="Some Title", + created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), + mime_type="application/pdf", + pk=2, + checksum="2", + archive_serial_number=25, + ) + + cf = CustomField.objects.create( + name="Invoice", + data_type=CustomField.FieldDataType.INT, + ) + + cf2 = CustomField.objects.create( + name="Select Field", + data_type=CustomField.FieldDataType.SELECT, + extra_data={"select_options": ["ChoiceOne", "ChoiceTwo"]}, + ) + + CustomFieldInstance.objects.create( + document=doc_a, + field=cf2, + value_select=0, + ) + + cfi = CustomFieldInstance.objects.create( + document=doc_a, + field=cf, + value_int=1234, + ) + + with override_settings( + FILENAME_FORMAT=""" + {% if "Invoice" in custom_fields %} + invoices/{{ custom_fields | get_cf_value('Invoice') }} + {% else %} + not-invoices/{{ title }} + {% endif %} + """, + ): + self.assertEqual( + generate_filename(doc_a), + "invoices/1234.pdf", + ) + + with override_settings( + FILENAME_FORMAT=""" + {% if "Select Field" in custom_fields %} + {{ title }}_{{ custom_fields | get_cf_value('Select Field') }} + {% else %} + {{ title }} + {% endif %} + """, + ): + self.assertEqual( + generate_filename(doc_a), + "Some Title_ChoiceOne.pdf", + ) + + cf.name = "Invoice Number" + cfi.value_int = 4567 + cfi.save() + cf.save() + + with override_settings( + FILENAME_FORMAT="invoices/{{ custom_fields | get_cf_value('Invoice Number') }}", + ): + self.assertEqual( + generate_filename(doc_a), + "invoices/4567.pdf", + ) + + with override_settings( + FILENAME_FORMAT="invoices/{{ custom_fields | get_cf_value('Ince Number', 0) }}", + ): + self.assertEqual( + generate_filename(doc_a), + "invoices/0.pdf", + ) + + def test_datetime_filter(self): + """ + GIVEN: + - Filename format with datetime filter + WHEN: + - Filepath for a document with this format is called + THEN: + - The datetime filter is rendered + """ + doc_a = Document.objects.create( + title="Some Title", + created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), + mime_type="application/pdf", + pk=2, + checksum="2", + archive_serial_number=25, + ) + + CustomField.objects.create( + name="Invoice Date", + data_type=CustomField.FieldDataType.DATE, + ) + CustomFieldInstance.objects.create( + document=doc_a, + field=CustomField.objects.get(name="Invoice Date"), + value_date=timezone.make_aware( + datetime.datetime(2024, 10, 1, 7, 36, 51, 153), + ), + ) + + with override_settings( + FILENAME_FORMAT="{{ created | datetime('%Y') }}/{{ title }}", + ): + self.assertEqual( + generate_filename(doc_a), + "2020/Some Title.pdf", + ) + + with override_settings( + FILENAME_FORMAT="{{ created | datetime('%Y-%m-%d') }}/{{ title }}", + ): + self.assertEqual( + generate_filename(doc_a), + "2020-06-25/Some Title.pdf", + ) + + with override_settings( + FILENAME_FORMAT="{{ custom_fields | get_cf_value('Invoice Date') | datetime('%Y-%m-%d') }}/{{ title }}", + ): + self.assertEqual( + generate_filename(doc_a), + "2024-10-01/Some Title.pdf", + ) diff --git a/src/documents/tests/test_migration_storage_path_template.py b/src/documents/tests/test_migration_storage_path_template.py new file mode 100644 index 000000000..37b87a115 --- /dev/null +++ b/src/documents/tests/test_migration_storage_path_template.py @@ -0,0 +1,30 @@ +from documents.models import StoragePath +from documents.tests.utils import TestMigrations + + +class TestMigrateStoragePathToTemplate(TestMigrations): + migrate_from = "1054_customfieldinstance_value_monetary_amount_and_more" + migrate_to = "1055_alter_storagepath_path" + + def setUpBeforeMigration(self, apps): + self.old_format = StoragePath.objects.create( + name="sp1", + path="Something/{title}", + ) + self.new_format = StoragePath.objects.create( + name="sp2", + path="{{asn}}/{{title}}", + ) + self.no_formatting = StoragePath.objects.create( + name="sp3", + path="Some/Fixed/Path", + ) + + def test_migrate_old_to_new_storage_path(self): + self.old_format.refresh_from_db() + self.new_format.refresh_from_db() + self.no_formatting.refresh_from_db() + + self.assertEqual(self.old_format.path, "Something/{{ title }}") + self.assertEqual(self.new_format.path, "{{asn}}/{{title}}") + self.assertEqual(self.no_formatting.path, "Some/Fixed/Path") From 3f27d289f35b1d55b0eb232e5a1f9aeadadd5391 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 6 Oct 2024 14:27:02 -0700 Subject: [PATCH 40/74] Fix: trigger change warning for saved views with default fields if changed (#7865) --- src-ui/messages.xlf | 14 +++++++------- .../document-list/document-list.component.ts | 12 +++++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 3d8d89c55..7838c63ed 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -319,7 +319,7 @@ src/app/components/document-list/document-list.component.ts - 128 + 138 src/app/components/manage/custom-fields/custom-fields.component.html @@ -6946,7 +6946,7 @@ src/app/components/document-list/document-list.component.ts - 245 + 255 @@ -6957,7 +6957,7 @@ src/app/components/document-list/document-list.component.ts - 238 + 248 @@ -7183,28 +7183,28 @@ Reset filters / selection src/app/components/document-list/document-list.component.ts - 226 + 236 Open first [selected] document src/app/components/document-list/document-list.component.ts - 254 + 264 View "" saved successfully. src/app/components/document-list/document-list.component.ts - 290 + 300 View "" created successfully. src/app/components/document-list/document-list.component.ts - 333 + 343 diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 75d80d659..1c559da05 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -15,7 +15,12 @@ import { isFullTextFilterRule, } from 'src/app/utils/filter-rules' import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' -import { DisplayField, DisplayMode, Document } from 'src/app/data/document' +import { + DEFAULT_DISPLAY_FIELDS, + DisplayField, + DisplayMode, + Document, +} from 'src/app/data/document' import { SavedView } from 'src/app/data/saved-view' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { @@ -108,6 +113,11 @@ export class DocumentListComponent (this.unmodifiedSavedView.display_fields && this.unmodifiedSavedView.display_fields.join(',') !== this.activeDisplayFields.join(',')) || + (!this.unmodifiedSavedView.display_fields && + this.activeDisplayFields.join(',') !== + DEFAULT_DISPLAY_FIELDS.filter((f) => f.id !== DisplayField.ADDED) + .map((f) => f.id) + .join(',')) || filterRulesDiffer( this.unmodifiedSavedView.filter_rules, this.list.filterRules From 2a162815c2da769eed4aa81aa797f2b14f0eb056 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:25:52 -0700 Subject: [PATCH 41/74] Fix: fix auto-dismiss completed tasks on open document (#7869) --- src-ui/src/app/services/tasks.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/services/tasks.service.ts b/src-ui/src/app/services/tasks.service.ts index ed14c8071..e2c064e03 100644 --- a/src-ui/src/app/services/tasks.service.ts +++ b/src-ui/src/app/services/tasks.service.ts @@ -67,7 +67,7 @@ export class TasksService { .post(`${this.baseUrl}acknowledge_tasks/`, { tasks: [...task_ids], }) - .pipe(takeUntil(this.unsubscribeNotifer), first()) + .pipe(first()) .subscribe((r) => { this.reload() }) From 52a74d4997d04e1e27000c29f16e1e5ccb4c36ac Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:47:37 -0700 Subject: [PATCH 42/74] Fixes the ASN checking to allow an ASN of 0 (#7878) --- src/documents/consumer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/documents/consumer.py b/src/documents/consumer.py index 803d82510..f79d3f9c3 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -348,7 +348,7 @@ class ConsumerPlugin( """ Check that if override_asn is given, it is unique and within a valid range """ - if not self.metadata.asn: + if self.metadata.asn is None: # check not necessary in case no ASN gets set return # Validate the range is above zero and less than uint32_t max @@ -905,7 +905,7 @@ class ConsumerPlugin( pk=self.metadata.storage_path_id, ) - if self.metadata.asn: + if self.metadata.asn is not None: document.archive_serial_number = self.metadata.asn if self.metadata.owner_id: From dea026330917df1e0bfccbdedcf3f64aa04ffdfc Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:36:09 -0700 Subject: [PATCH 43/74] Chore: fix test comments --- src/documents/tests/test_workflows.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index c410f76a2..c5d975958 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -306,11 +306,11 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase): def test_workflow_match_multiple(self): """ GIVEN: - - Multiple existing workflow + - Multiple existing workflows WHEN: - File that matches is consumed THEN: - - Template overrides are applied with subsequent templates overwriting previous values + - Workflow overrides are applied with subsequent workflows overwriting previous values or merging if multiple """ trigger1 = WorkflowTrigger.objects.create( @@ -373,12 +373,12 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase): None, ) document = Document.objects.first() - # template 1 + # workflow 1 self.assertEqual(document.document_type, self.dt) - # template 2 + # workflow 2 self.assertEqual(document.correspondent, self.c2) self.assertEqual(document.storage_path, self.sp) - # template 1 & 2 + # workflow 1 & 2 self.assertEqual( list(document.tags.all()), [self.t1, self.t2, self.t3], From b8111df9635263c9d4c780a279d2f96468d632b6 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:35:36 -0700 Subject: [PATCH 44/74] Feature: live preview of storage path (#7870) --- src-ui/messages.xlf | 92 ++++++++------ .../storage-path-edit-dialog.component.html | 52 +++++++- .../storage-path-edit-dialog.component.scss | 4 + ...storage-path-edit-dialog.component.spec.ts | 96 ++++++++++++++- .../storage-path-edit-dialog.component.ts | 115 ++++++++++++++++-- .../workflow-edit-dialog.component.scss | 4 + .../input/textarea/textarea.component.html | 2 +- .../rest/storage-path.service.spec.ts | 28 +++++ .../app/services/rest/storage-path.service.ts | 8 ++ src-ui/src/styles.scss | 7 +- src/documents/serialisers.py | 15 +++ src/documents/templating/filepath.py | 1 - src/documents/tests/test_api_objects.py | 29 +++++ src/documents/views.py | 21 ++++ src/paperless/urls.py | 6 + 15 files changed, 426 insertions(+), 54 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 7838c63ed..8a7d70981 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -568,7 +568,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 29 + 79 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -700,6 +700,10 @@ src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html 35 + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 50 + src/app/components/common/input/document-link/document-link.component.html 51 @@ -1680,7 +1684,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 28 + 78 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -3500,7 +3504,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 14 + 64 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -3519,7 +3523,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 16 + 66 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -3538,7 +3542,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 19 + 69 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -4126,39 +4130,72 @@ 42 - - e.g. + + See <a target='_blank' href='https://docs.paperless-ngx.com/advanced_usage/#file-name-handling'>the documentation</a>. - src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts - 28 + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 13 - - or use slashes to add directories e.g. + + Preview - src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 18 + + + src/app/components/document-detail/document-detail.component.html + 282 + + + + Path test failed + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html 30 - - See <a target="_blank" href="https://docs.paperless-ngx.com/advanced_usage/#file-name-handling">documentation</a> for full list. + + No document selected - src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html 32 + + Search for documents + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 38 + + + src/app/components/common/input/document-link/document-link.component.ts + 53 + + + + No documents found + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 39 + + + src/app/components/common/input/document-link/document-link.component.ts + 44 + + Create new storage path src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts - 37 + 63 Edit storage path src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts - 41 + 67 @@ -4836,20 +4873,6 @@ 14 - - No documents found - - src/app/components/common/input/document-link/document-link.component.ts - 44 - - - - Search for documents - - src/app/components/common/input/document-link/document-link.component.ts - 53 - - Selected items @@ -6120,13 +6143,6 @@ 275 - - Preview - - src/app/components/document-detail/document-detail.component.html - 282 - - Notes diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html index f8232f957..45b2bc5e9 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html @@ -10,7 +10,57 @@