From 1e5cba477dee7939c35771333f1338597f5055f2 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 23 Nov 2023 14:09:34 +0100 Subject: [PATCH 01/66] [us40] Improve the layout to have the buttons above and to make the category filter take less space --- src/app/app.module.ts | 3 +- src/app/file-list/file-list.component.html | 164 ++++++++++++--------- src/app/file-list/file-list.component.scss | 32 ++++ src/app/file-list/file-list.component.ts | 1 + src/app/footer/footer.component.scss | 1 - src/app/homepage/homepage.component.html | 2 +- src/app/homepage/homepage.component.scss | 2 +- 7 files changed, 130 insertions(+), 75 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 36b6361..0784aa5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -73,4 +73,5 @@ import {MatSortModule} from "@angular/material/sort"; ], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule { +} diff --git a/src/app/file-list/file-list.component.html b/src/app/file-list/file-list.component.html index eabdebf..b83cd9f 100644 --- a/src/app/file-list/file-list.component.html +++ b/src/app/file-list/file-list.component.html @@ -1,79 +1,101 @@ -
Categories
- - - {{node.name}} - - -
- - {{node.name}}
-
- +
+ category +   + Categories
- - - - Filter - - - - - - - + + + {{node.name}} + + +
+ + {{node.name}} +
+
+ +
+
+
+ + +
+ + Filter + + +
Namefile icon -   {{element.name}}
+ + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - - -
Namefile icon +   {{element.name}} Date {{element.date | date:'medium'}} Date {{element.date | date:'medium'}} Category - - {{cat.name}} - - Category + + {{cat.name}} + + Size {{element.size | filesize}} Size {{element.size | filesize}} - more_vert - - - download - Download - - - - - + more_vert + + + download + Download + + + + +
No document matching the file name "{{nameFilter}}"
+ + + + No document matching the file name "{{nameFilter}}" + + +
+ diff --git a/src/app/file-list/file-list.component.scss b/src/app/file-list/file-list.component.scss index c3a10b5..7506bfe 100644 --- a/src/app/file-list/file-list.component.scss +++ b/src/app/file-list/file-list.component.scss @@ -27,3 +27,35 @@ mat-tree-node { .not_found { padding: 28px; } + +.categoryFilters { + padding-right: 20px; +} + +.example-spacer { + flex: 1 1 auto; +} + +.categoryExpand { + display: flex; +} + +.categoryFilterHeader { + padding: 8px; + display: flex; + align-items: center; +} + +.files-list { + min-height: 800px; + flex-grow: 1; +} + +.file-list-and-categories { + display: flex; +} + +.category-filter-panel { + background-color: white; +} + diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index 05cbf0e..8b12011 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -60,6 +60,7 @@ export class FileListComponent implements OnInit { categoryTreeControl = new NestedTreeControl(node => this.getChildren(node.id)); // Static is simpler here to avoid change detection stability issues @ViewChild(MatSort, {static: true}) fileSort?: MatSort; + isCategoryPanelExpanded = true; private categoryFilters = new Set(); constructor(private fileService: FileService, private baseFolderService: BaseFolderService, public dialog: MatDialog) { diff --git a/src/app/footer/footer.component.scss b/src/app/footer/footer.component.scss index dedd772..48afff6 100644 --- a/src/app/footer/footer.component.scss +++ b/src/app/footer/footer.component.scss @@ -1,5 +1,4 @@ mat-toolbar { - margin-top: 20px; font-size: 12px; } diff --git a/src/app/homepage/homepage.component.html b/src/app/homepage/homepage.component.html index c443bad..8356dc6 100644 --- a/src/app/homepage/homepage.component.html +++ b/src/app/homepage/homepage.component.html @@ -1,5 +1,5 @@ -
+ diff --git a/src/app/homepage/homepage.component.scss b/src/app/homepage/homepage.component.scss index 34f2c31..6038a74 100644 --- a/src/app/homepage/homepage.component.scss +++ b/src/app/homepage/homepage.component.scss @@ -1,4 +1,4 @@ .smd-main-content { - padding: 20px 70px 0; + padding: 20px 70px; } From ddf66535a2ae78599bef8703484ee82155313250 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 23 Nov 2023 15:07:34 +0100 Subject: [PATCH 02/66] [us40] Add a 'rules' page --- src/app/app-routing.module.spec.ts | 73 ++++++++++++++++++++++++ src/app/app-routing.module.ts | 6 ++ src/app/app.module.ts | 4 +- src/app/homepage/homepage.component.html | 2 + src/app/rules/rules.component.html | 1 + src/app/rules/rules.component.scss | 0 src/app/rules/rules.component.spec.ts | 12 ++++ src/app/rules/rules.component.ts | 10 ++++ 8 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/app/app-routing.module.spec.ts create mode 100644 src/app/rules/rules.component.html create mode 100644 src/app/rules/rules.component.scss create mode 100644 src/app/rules/rules.component.spec.ts create mode 100644 src/app/rules/rules.component.ts diff --git a/src/app/app-routing.module.spec.ts b/src/app/app-routing.module.spec.ts new file mode 100644 index 0000000..a05167d --- /dev/null +++ b/src/app/app-routing.module.spec.ts @@ -0,0 +1,73 @@ +import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; +import {Router, RouterModule, RouterOutlet} from "@angular/router"; +import {RouterTestingModule} from "@angular/router/testing"; +import {AppModule} from "./app.module"; +import {Location} from '@angular/common'; +import {fakeAsync, tick} from "@angular/core/testing"; +import {RulesComponent} from "./rules/rules.component"; +import {GoogleDriveAuthService} from "./file-upload/google-drive-auth.service"; +import {mock, when} from "strong-mock"; +import {HomepageComponent} from "./homepage/homepage.component"; + +describe('AppRoutingModule', () => { + beforeEach(() => { + return MockBuilder( + [ + RouterModule, + RouterTestingModule.withRoutes([]) + ], + AppModule, + ); + }); + describe('when logged in', () => { + beforeEach(() => { + // Mock that the user is logged in + let authService = mock(); + MockInstance(GoogleDriveAuthService, () => { + return { + isAuthenticatedAndHasValidApiToken: authService.isAuthenticatedAndHasValidApiToken + } + }); + when(() => authService.isAuthenticatedAndHasValidApiToken()).thenReturn(true); + }) + + it('should display rules page', fakeAsync(() => { + // Arrange + const fixture = MockRender(RouterOutlet, {}); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); + + // Act + location.go('/rules'); + + // Assert + if (fixture.ngZone) { + fixture.ngZone.run(() => router.initialNavigation()); + tick(); // is needed for rendering of the current route. + } + + expect(location.path()).toEqual('/rules'); + expect(() => ngMocks.find(RulesComponent)).not.toThrow(); + })); + + it('should display home page', fakeAsync(() => { + // Arrange + const fixture = MockRender(RouterOutlet, {}); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); + + // Act + location.go('/'); + + // Assert + if (fixture.ngZone) { + fixture.ngZone.run(() => router.initialNavigation()); + tick(); // is needed for rendering of the current route. + } + + expect(location.path()).toEqual('/'); + expect(() => ngMocks.find(HomepageComponent)).not.toThrow(); + })); + }) + +}); diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7f5a735..b9db162 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,6 +3,7 @@ import {RouterModule, Routes} from '@angular/router'; import {HomepageComponent} from "./homepage/homepage.component"; import {authGuard} from "./auth/auth.guard"; import {LoginComponent} from "./login/login.component"; +import {RulesComponent} from "./rules/rules.component"; const routes: Routes = [ { @@ -10,6 +11,11 @@ const routes: Routes = [ component: HomepageComponent, canActivate: [authGuard] }, + { + path: 'rules', + component: RulesComponent, + canActivate: [authGuard] + }, { path: 'login', component: LoginComponent diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0784aa5..c42e6d5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,6 +31,7 @@ import {MatTreeModule} from "@angular/material/tree"; import {MatChipsModule} from "@angular/material/chips"; import {TitleHeaderComponent} from './title-header/title-header.component'; import {MatSortModule} from "@angular/material/sort"; +import {RulesComponent} from './rules/rules.component'; @NgModule({ declarations: [ @@ -42,7 +43,8 @@ import {MatSortModule} from "@angular/material/sort"; FooterComponent, FileListComponent, LoginComponent, - TitleHeaderComponent + TitleHeaderComponent, + RulesComponent ], imports: [ BrowserModule, diff --git a/src/app/homepage/homepage.component.html b/src/app/homepage/homepage.component.html index 8356dc6..4b5e758 100644 --- a/src/app/homepage/homepage.component.html +++ b/src/app/homepage/homepage.component.html @@ -1,5 +1,7 @@
+ Setup rules +  
diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html new file mode 100644 index 0000000..4b66e66 --- /dev/null +++ b/src/app/rules/rules.component.html @@ -0,0 +1 @@ +

rules works!

diff --git a/src/app/rules/rules.component.scss b/src/app/rules/rules.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts new file mode 100644 index 0000000..dadc031 --- /dev/null +++ b/src/app/rules/rules.component.spec.ts @@ -0,0 +1,12 @@ +import {RulesComponent} from './rules.component'; +import {MockBuilder, MockRender} from "ng-mocks"; +import {AppModule} from "../app.module"; + +describe('RulesComponent', () => { + beforeEach(() => MockBuilder(RulesComponent, AppModule)); + + it('should create', () => { + let component = MockRender(RulesComponent).point.componentInstance; + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts new file mode 100644 index 0000000..152e371 --- /dev/null +++ b/src/app/rules/rules.component.ts @@ -0,0 +1,10 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-rules', + templateUrl: './rules.component.html', + styleUrls: ['./rules.component.scss'] +}) +export class RulesComponent { + +} From 2d46c83fb92ff68c78df43794fbdfe0bb0f32404 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 23 Nov 2023 15:09:24 +0100 Subject: [PATCH 03/66] [us40] Fix padding on login page --- src/app/login/login.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/login/login.component.scss b/src/app/login/login.component.scss index dbdb740..a338ecf 100644 --- a/src/app/login/login.component.scss +++ b/src/app/login/login.component.scss @@ -1,3 +1,3 @@ .smd-main-content { - padding: 20px 70px 0; + padding: 20px 70px; } From ca100559990b7f1a95c857823697bf279aa770fd Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 23 Nov 2023 15:14:14 +0100 Subject: [PATCH 04/66] [us40] Add a go back button to the rules page --- src/app/rules/rules.component.html | 8 +++++++- src/app/rules/rules.component.scss | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 4b66e66..55d6327 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -1 +1,7 @@ -

rules works!

+ +
+ Go back +
+
+

Setup rules

+
diff --git a/src/app/rules/rules.component.scss b/src/app/rules/rules.component.scss index e69de29..82f29a6 100644 --- a/src/app/rules/rules.component.scss +++ b/src/app/rules/rules.component.scss @@ -0,0 +1,7 @@ +.smd-main-content { + padding: 20px 70px; +} + +.goBackButton { + padding: 20px; +} From a4b4f197d512544931365e98818d227cdcdad3b2 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 23 Nov 2023 16:20:39 +0100 Subject: [PATCH 05/66] [us40] Add two hardcoded rules to show --- src/app/app.module.ts | 4 ++- src/app/rules/rules.component.html | 15 ++++++++++++ src/app/rules/rules.component.scss | 4 +++ src/app/rules/rules.component.spec.ts | 35 ++++++++++++++++++++++++++- src/app/rules/rules.component.ts | 15 ++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c42e6d5..d9429a5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -32,6 +32,7 @@ import {MatChipsModule} from "@angular/material/chips"; import {TitleHeaderComponent} from './title-header/title-header.component'; import {MatSortModule} from "@angular/material/sort"; import {RulesComponent} from './rules/rules.component'; +import {MatExpansionModule} from "@angular/material/expansion"; @NgModule({ declarations: [ @@ -68,7 +69,8 @@ import {RulesComponent} from './rules/rules.component'; FormsModule, MatTreeModule, MatChipsModule, - MatSortModule + MatSortModule, + MatExpansionModule ], providers: [ httpInterceptorProviders diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 55d6327..1cd7271 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -4,4 +4,19 @@

Setup rules

+ + + + + {{rule.name}} + + + {{rule.description}} + + +
+ {{rule.script}} +
+
+
diff --git a/src/app/rules/rules.component.scss b/src/app/rules/rules.component.scss index 82f29a6..c581f24 100644 --- a/src/app/rules/rules.component.scss +++ b/src/app/rules/rules.component.scss @@ -5,3 +5,7 @@ .goBackButton { padding: 20px; } + +.scriptFormField { + width: 100%; +} diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index dadc031..1945d6a 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -1,5 +1,5 @@ import {RulesComponent} from './rules.component'; -import {MockBuilder, MockRender} from "ng-mocks"; +import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; describe('RulesComponent', () => { @@ -9,4 +9,37 @@ describe('RulesComponent', () => { let component = MockRender(RulesComponent).point.componentInstance; expect(component).toBeTruthy(); }); + + it('should list two rules', () => { + // Act + MockRender(RulesComponent); + + // Assert + expect(Page.getRuleNames()).toEqual(['Electric bill', 'Bank account statement']); + expect(Page.getRuleDescription('Electric bill')).toEqual('Detect electric bills'); + expect(Page.getRuleScript('Electric bill')).toEqual('return fileName === "electric_bill.pdf"'); + }) }); + +class Page { + static getRuleNames(): string[] { + return ngMocks.findAll("mat-panel-title") + .map(row => row.nativeNode.textContent.trim()); + } + + static getRuleDescription(name: string): string { + let rule = ngMocks.findAll("mat-panel-title") + .find(row => row.nativeNode.textContent.trim() === name) + ?.parent; + return ngMocks.find(rule, 'mat-panel-description') + .nativeNode.textContent.trim(); + } + + static getRuleScript(name: string): string { + let rule = ngMocks.findAll("mat-panel-title") + .find(row => row.nativeNode.textContent.trim() === name) + ?.parent?.parent; + return ngMocks.find(rule, '.ruleScript') + .nativeNode.textContent.trim(); + } +} diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 152e371..d4901c6 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -1,10 +1,25 @@ import {Component} from '@angular/core'; +interface Rule { + name: string; + description: string; + script: string; +} + @Component({ selector: 'app-rules', templateUrl: './rules.component.html', styleUrls: ['./rules.component.scss'] }) export class RulesComponent { + rules: Rule[] = [{ + name: 'Electric bill', + description: 'Detect electric bills', + script: 'return fileName === "electric_bill.pdf"' + }, { + name: 'Bank account statement', + description: '...', + script: 'return fileName === "bank_account_statement.pdf"' + }]; } From ced0a1e97058625fb5325399f4418d0964bc9ad4 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 23 Nov 2023 16:28:13 +0100 Subject: [PATCH 06/66] [us40] Fix failing test in FileListComponent due to missing fixture.detectChanges --- src/app/file-list/file-list.component.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 7dfda24..74b3c22 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -627,12 +627,13 @@ describe('FileListComponent', () => { it('should filter out one item out of two items', () => { // Arrange mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); - MockRender(FileListComponent); + let fixture = MockRender(FileListComponent); // Act Page.selectCategoryFilter('Cat1Child'); // Assert + fixture.detectChanges(); let actionsRow = 'more_vert'; let expected = [['name2', 'Cat1Cat1Child', 'Aug 3, 2023, 2:54:55 PM', '1.75 kB', actionsRow]]; expect(Page.getTableRows()).toEqual(expected); @@ -642,12 +643,13 @@ describe('FileListComponent', () => { // Arrange mockTxtAndImageFiles(); - MockRender(FileListComponent); + let fixture = MockRender(FileListComponent); // Act Page.selectCategoryFilter('Image'); // Assert + fixture.detectChanges(); expect(Page.getDisplayedFileNames()).toEqual(['avatar.png', 'default.png', 'funny.png']) }) From 000ae75cd48e9b96ef24c056544282446bf3ebf8 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 23 Nov 2023 16:55:34 +0100 Subject: [PATCH 07/66] [us40] Add a Run all rules button on rules page --- src/app/rules/rule.service.spec.ts | 15 +++++++++++ src/app/rules/rule.service.ts | 10 +++++++ src/app/rules/rules.component.html | 2 ++ src/app/rules/rules.component.spec.ts | 38 ++++++++++++++++++++++++++- src/app/rules/rules.component.ts | 7 +++++ 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/app/rules/rule.service.spec.ts create mode 100644 src/app/rules/rule.service.ts diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts new file mode 100644 index 0000000..bf90dd5 --- /dev/null +++ b/src/app/rules/rule.service.spec.ts @@ -0,0 +1,15 @@ +import {RuleService} from './rule.service'; +import {MockBuilder, MockRender} from "ng-mocks"; +import {AppModule} from "../app.module"; + +describe('RuleService', () => { + beforeEach(() => MockBuilder(RuleService, AppModule)); + + it('should be created', () => { + // Act + const service = MockRender(RuleService).point.componentInstance; + + // Assert + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts new file mode 100644 index 0000000..2e67ce7 --- /dev/null +++ b/src/app/rules/rule.service.ts @@ -0,0 +1,10 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class RuleService { + runAll() { + return undefined; + } +} diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 1cd7271..f0f9c0c 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -1,6 +1,8 @@
Go back +   +

Setup rules

diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 1945d6a..63cead5 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -1,6 +1,12 @@ import {RulesComponent} from './rules.component'; -import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; +import {RuleService} from "./rule.service"; +import {mock, when} from "strong-mock"; +import {MatButtonHarness} from "@angular/material/button/testing"; +import {HarnessLoader} from "@angular/cdk/testing"; +import {ComponentFixture} from "@angular/core/testing"; +import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; describe('RulesComponent', () => { beforeEach(() => MockBuilder(RulesComponent, AppModule)); @@ -19,9 +25,34 @@ describe('RulesComponent', () => { expect(Page.getRuleDescription('Electric bill')).toEqual('Detect electric bills'); expect(Page.getRuleScript('Electric bill')).toEqual('return fileName === "electric_bill.pdf"'); }) + + it('should run all the rules when clicking on "run rules" button', async () => { + // Arrange + let ruleService = mock(); + MockInstance(RuleService, () => { + return { + runAll: ruleService.runAll + } + }); + when(() => ruleService.runAll()).thenReturn(undefined); + let fixture = MockRender(RulesComponent); + let page = new Page(fixture); + + // Act + await page.clickOnRunRulesButton(); + + // Assert + // no failure from mock setup + }) }); class Page { + private loader: HarnessLoader; + + constructor(fixture: ComponentFixture) { + this.loader = TestbedHarnessEnvironment.loader(fixture); + } + static getRuleNames(): string[] { return ngMocks.findAll("mat-panel-title") .map(row => row.nativeNode.textContent.trim()); @@ -42,4 +73,9 @@ class Page { return ngMocks.find(rule, '.ruleScript') .nativeNode.textContent.trim(); } + + async clickOnRunRulesButton() { + let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Run all'})); + return button.click(); + } } diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index d4901c6..dd5d57a 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -1,4 +1,5 @@ import {Component} from '@angular/core'; +import {RuleService} from "./rule.service"; interface Rule { name: string; @@ -22,4 +23,10 @@ export class RulesComponent { script: 'return fileName === "bank_account_statement.pdf"' }]; + constructor(private ruleService: RuleService) { + } + + runAll() { + this.ruleService.runAll(); + } } From dd077919d217ff838772c69a9ed267b4e7688fc5 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 24 Nov 2023 13:45:33 +0100 Subject: [PATCH 08/66] [us40] Implement runAll to automatically categorize a file --- src/app/file-list/file-list.component.spec.ts | 2 +- src/app/file-list/file-list.component.ts | 2 +- src/app/file-upload/base-folder.service.ts | 1 + src/app/rules/rule.service.spec.ts | 47 ++++++++++- src/app/rules/rule.service.ts | 80 ++++++++++++++++++- src/app/rules/rules.component.html | 2 +- src/app/rules/rules.component.spec.ts | 9 ++- src/app/rules/rules.component.ts | 26 +++--- 8 files changed, 147 insertions(+), 22 deletions(-) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 74b3c22..83ec7d0 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -753,7 +753,7 @@ describe('FileListComponent', () => { }) }); -function mockFileElement(name: string, parentId: string = 'baseFolderId', id: string | undefined = undefined, size: number = 0, date: string = ''): FileElement { +export function mockFileElement(name: string, parentId: string = 'baseFolderId', id: string | undefined = undefined, size: number = 0, date: string = ''): FileElement { if (!id) { id = name + '-' + uuid(); } diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index 8b12011..ed60e8b 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -34,7 +34,7 @@ export interface FileElement extends FileOrFolderElement { dlLink: string; } -function isFileElement(object: FileOrFolderElement): object is FileElement { +export function isFileElement(object: FileOrFolderElement): object is FileElement { return 'size' in object; } diff --git a/src/app/file-upload/base-folder.service.ts b/src/app/file-upload/base-folder.service.ts index 8cfecbe..8bcc1c0 100644 --- a/src/app/file-upload/base-folder.service.ts +++ b/src/app/file-upload/base-folder.service.ts @@ -10,6 +10,7 @@ export class BaseFolderService { constructor(private fileService: FileService) { } + // TODO: persist the base folder id for quick access findOrCreateBaseFolder() { return this.fileService.findOrCreateFolder(BaseFolderService.BASE_FOLDER_NAME); } diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index bf90dd5..7a1cf69 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -1,6 +1,12 @@ import {RuleService} from './rule.service'; -import {MockBuilder, MockRender} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; +import {mockFileService} from "../file-list/file.service.spec"; +import {mock, when} from "strong-mock"; +import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; +import {fakeAsync, tick} from "@angular/core/testing"; +import {mockFileElement} from "../file-list/file-list.component.spec"; +import {BaseFolderService} from "../file-upload/base-folder.service"; describe('RuleService', () => { beforeEach(() => MockBuilder(RuleService, AppModule)); @@ -12,4 +18,43 @@ describe('RuleService', () => { // Assert expect(service).toBeTruthy(); }); + + describe('runAll', () => { + it('should automatically categorize a file', fakeAsync(() => { + // Arrange + let baseFolderService = mock(); + MockInstance(BaseFolderService, () => { + return { + findOrCreateBaseFolder: baseFolderService.findOrCreateBaseFolder + } + }); + when(() => baseFolderService.findOrCreateBaseFolder()) + .thenReturn(mustBeConsumedAsyncObservable('baseFolderId')); + + let fileService = mockFileService(); + + when(() => fileService.findOrCreateFolder("Electricity", "baseFolderId")) + .thenReturn(mustBeConsumedAsyncObservable('elecCatId548')); + + when(() => fileService.findOrCreateFolder("Bills", "elecCatId548")) + .thenReturn(mustBeConsumedAsyncObservable('billsCatId489')); + + let file = mockFileElement('electricity_bill.pdf'); + when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([file])) + + + // The file should be set to the bills category + when(() => fileService.setCategory(file.id, 'billsCatId489')) + .thenReturn(mustBeConsumedAsyncObservable(undefined)); + + const service = MockRender(RuleService).point.componentInstance; + + // Act + service.runAll().subscribe(); + + // Assert + tick(); + // No failure in mock setup + })); + }) }); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 2e67ce7..6d0596f 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,10 +1,86 @@ import {Injectable} from '@angular/core'; +import {Rule, SAMPLE_RULES} from "./rules.component"; +import {FileService} from "../file-list/file.service"; +import {map, mergeMap, Observable, of, zip} from "rxjs"; +import {FileElement, isFileElement} from "../file-list/file-list.component"; +import {BaseFolderService} from "../file-upload/base-folder.service"; @Injectable({ providedIn: 'root' }) export class RuleService { - runAll() { - return undefined; + constructor(private fileService: FileService, private baseFolderService: BaseFolderService) { + } + + runAll(): Observable { + let rules = SAMPLE_RULES; + return this.fileService.findAll() + .pipe(mergeMap(fileOrFolders => { + // Get all files + let files = fileOrFolders.filter(file => isFileElement(file)) + .map(value => value as FileElement); + + // Run the script for each file to get the associated category + let fileToCategoryMap = this.computeFileToCategoryMap(files, rules); + + // Set the computed category for each files + return this.setAllFileCategory(fileToCategoryMap); + })); + } + + /** + * Run the given rules on the given files and return the associated category for each file that got a matching rule + */ + private computeFileToCategoryMap(files: FileElement[], rules: Rule[]) { + let fileToCategoryMap = new Map(); + + files.forEach(file => { + // Find the first rule which matches + let rule = rules.find(rule => { + return this.run(rule, file); + }) + if (rule) { + fileToCategoryMap.set(file, rule.category); + } + }); + + return fileToCategoryMap; + } + + private run(rule: Rule, file: FileElement) { + return Function("const fileName = arguments[0];" + rule.script)(file.name); + } + + /** + * Find or create the categories for each file and associate them + */ + private setAllFileCategory(fileToCategoryMap: Map) { + return this.baseFolderService.findOrCreateBaseFolder() + .pipe(mergeMap(baseFolderId => { + let categoryRequests: Observable[] = []; + fileToCategoryMap + .forEach((category, file) => { + categoryRequests.push(this.findOrCreateCategories(category, baseFolderId) + .pipe(mergeMap(categoryId => { + return this.fileService.setCategory(file.id, categoryId); + }))); + }); + let observable = zip(categoryRequests); + return observable + .pipe(map(() => { + })); + })); + } + + // TODO: move and refactor duplicate to FileService + private findOrCreateCategories(categories: string[], categoryId: string): Observable { + let categoryName = categories.shift(); + if (categoryName !== undefined) { + return this.fileService.findOrCreateFolder(categoryName, categoryId) + .pipe(mergeMap(newCategoryId => { + return this.findOrCreateCategories(categories, newCategoryId); + })); + } + return of(categoryId); } } diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index f0f9c0c..4b30d57 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -13,7 +13,7 @@

Setup rules

{{rule.name}} - {{rule.description}} +  > {{cat}}
diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 63cead5..48cd7e8 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -7,6 +7,7 @@ import {MatButtonHarness} from "@angular/material/button/testing"; import {HarnessLoader} from "@angular/cdk/testing"; import {ComponentFixture} from "@angular/core/testing"; import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; +import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; describe('RulesComponent', () => { beforeEach(() => MockBuilder(RulesComponent, AppModule)); @@ -22,8 +23,8 @@ describe('RulesComponent', () => { // Assert expect(Page.getRuleNames()).toEqual(['Electric bill', 'Bank account statement']); - expect(Page.getRuleDescription('Electric bill')).toEqual('Detect electric bills'); - expect(Page.getRuleScript('Electric bill')).toEqual('return fileName === "electric_bill.pdf"'); + expect(Page.getRuleCategory('Electric bill')).toEqual('Electricity  > Bills'); + expect(Page.getRuleScript('Electric bill')).toEqual('return fileName === "electricity_bill.pdf"'); }) it('should run all the rules when clicking on "run rules" button', async () => { @@ -34,7 +35,7 @@ describe('RulesComponent', () => { runAll: ruleService.runAll } }); - when(() => ruleService.runAll()).thenReturn(undefined); + when(() => ruleService.runAll()).thenReturn(mustBeConsumedAsyncObservable(undefined)); let fixture = MockRender(RulesComponent); let page = new Page(fixture); @@ -58,7 +59,7 @@ class Page { .map(row => row.nativeNode.textContent.trim()); } - static getRuleDescription(name: string): string { + static getRuleCategory(name: string): string { let rule = ngMocks.findAll("mat-panel-title") .find(row => row.nativeNode.textContent.trim() === name) ?.parent; diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index dd5d57a..073a699 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -1,32 +1,34 @@ import {Component} from '@angular/core'; import {RuleService} from "./rule.service"; -interface Rule { +export interface Rule { name: string; - description: string; + category: string[]; script: string; } +export const SAMPLE_RULES: Rule[] = [{ + name: 'Electric bill', + category: ['Electricity', 'Bills'], + script: 'return fileName === "electricity_bill.pdf"' +}, { + name: 'Bank account statement', + category: ['Bank', 'Account statement'], + script: 'return false' +}]; + @Component({ selector: 'app-rules', templateUrl: './rules.component.html', styleUrls: ['./rules.component.scss'] }) export class RulesComponent { - rules: Rule[] = [{ - name: 'Electric bill', - description: 'Detect electric bills', - script: 'return fileName === "electric_bill.pdf"' - }, { - name: 'Bank account statement', - description: '...', - script: 'return fileName === "bank_account_statement.pdf"' - }]; + rules = SAMPLE_RULES; constructor(private ruleService: RuleService) { } runAll() { - this.ruleService.runAll(); + this.ruleService.runAll().subscribe(); } } From 5d0e86344f1cd914a1ff7ec52b49d5c6a2a443d0 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 24 Nov 2023 15:23:23 +0100 Subject: [PATCH 09/66] [us40] RuleService.runAll: don't categorize a file which is already in the correct category --- src/app/file-list/file-list.component.spec.ts | 152 +++++++++--------- .../file-upload/base-folder.service.spec.ts | 17 +- .../file-upload/file-upload.service.spec.ts | 10 +- src/app/rules/rule.service.spec.ts | 55 ++++--- src/app/rules/rule.service.ts | 18 ++- src/testing/common-testing-function.spec.ts | 11 +- 6 files changed, 148 insertions(+), 115 deletions(-) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 83ec7d0..b6436d8 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -14,7 +14,6 @@ import {MatMenuHarness} from "@angular/material/menu/testing"; import {MatMenuModule} from "@angular/material/menu"; import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; import {findAsyncSequential, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; -import {BaseFolderService} from "../file-upload/base-folder.service"; import {MatInputHarness} from "@angular/material/input/testing"; import {MatButtonHarness} from "@angular/material/button/testing"; import {MatDialogModule} from "@angular/material/dialog"; @@ -30,6 +29,14 @@ import {MatAutocompleteHarness} from "@angular/material/autocomplete/testing"; import {MatChipGridHarness} from "@angular/material/chips/testing"; import {mockFileService} from "./file.service.spec"; import {MatSortModule} from "@angular/material/sort"; +import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; + +function mockRenderAndWaitForChanges() { + let fixture = MockRender(FileListComponent); + tick(); + fixture.detectChanges(); + return fixture; +} describe('FileListComponent', () => { beforeEach(() => MockBuilder(FileListComponent, AppModule) @@ -47,9 +54,7 @@ describe('FileListComponent', () => { it('should create (no element)', fakeAsync(() => { // Arrange - let findOrCreateBaseFolderMock = MockInstance(BaseFolderService, 'findOrCreateBaseFolder', - mock()); - when(() => findOrCreateBaseFolderMock()).thenReturn(of('baseFolderId')); + mockBaseFolderService(); let listMock = MockInstance(FileService, 'findAll', mock()); when(() => listMock()).thenReturn(mustBeConsumedAsyncObservable([])); @@ -63,21 +68,21 @@ describe('FileListComponent', () => { expect(Page.getTableRows()).toEqual([]); })); - it('should list two items', () => { + it('should list two items', fakeAsync(() => { // Arrange mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); // Act - MockRender(FileListComponent) + mockRenderAndWaitForChanges() // Assert let actionsRow = 'more_vert'; let expected = [['name1', 'Cat1', 'Aug 14, 2023, 2:48:44 PM', '1.42 MB', actionsRow], ['name2', 'Cat1Cat1Child', 'Aug 3, 2023, 2:54:55 PM', '1.75 kB', actionsRow]]; expect(Page.getTableRows()).toEqual(expected); - }) + })) - it('should sort items by name', () => { + it('should sort items by name', fakeAsync(() => { // Arrange let itemsAndCategories = []; itemsAndCategories.push(mockFileElement('za1')); @@ -87,13 +92,13 @@ describe('FileListComponent', () => { mockListItemsAndCategories(itemsAndCategories); // Act - MockRender(FileListComponent) + mockRenderAndWaitForChanges(); // Assert expect(Page.getDisplayedFileNames()).toEqual(['ab5', 'cd4', 'cd5', 'za1']); - }) + })) - it('should sort categories by name', () => { + it('should sort categories by name', fakeAsync(() => { // Arrange let itemsAndCategories = []; itemsAndCategories.push(mockFolderElement('za1')); @@ -104,11 +109,11 @@ describe('FileListComponent', () => { mockListItemsAndCategories(itemsAndCategories); // Act - MockRender(FileListComponent) + mockRenderAndWaitForChanges(); // Assert expect(Page.getCategories()).toEqual(['ab5', 'cd4', 'cd5', 'za1']); - }) + })) it('should trash an item then refresh', fakeAsync(async () => { // Arrange @@ -126,7 +131,7 @@ describe('FileListComponent', () => { }; when(() => fileService.findAll()).thenReturn(of([el1])) - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -159,7 +164,7 @@ describe('FileListComponent', () => { // We expect a last refresh after trashing the category when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([fileElementAfterRefresh])); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -182,21 +187,20 @@ describe('FileListComponent', () => { expect(Page.getCategories()).toEqual(['Cat1', 'Cat1Child', 'Cat2']) })) - it('should not list base folder as category in row categories', () => { + it('should not list base folder as category in row categories', fakeAsync(() => { // Arrange let baseFolder = mockFolderElement('BaseFolder', 'rootId', 'baseFolderId'); let el1 = mockFileElement('name1', baseFolder.id, 'id1', 1421315, '2023-08-14T14:48:44.928Z'); mockListItemsAndCategories([baseFolder, el1]); // Act - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Assert - fixture.detectChanges(); let actionsRow = 'more_vert'; let expected = [['name1', '', 'Aug 14, 2023, 2:48:44 PM', '1.42 MB', actionsRow]]; expect(Page.getTableRows()).toEqual(expected); - }) + })) describe('Category assignment', () => { it('should refresh after assigning a category to a file', fakeAsync(async () => { @@ -219,7 +223,7 @@ describe('FileListComponent', () => { let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); when(() => setCategoryMock(el2.id, 'cat848Id')).thenReturn(of(undefined)); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -239,7 +243,7 @@ describe('FileListComponent', () => { // Arrange mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -258,7 +262,7 @@ describe('FileListComponent', () => { // Arrange mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); Page.openItemMenu('name2'); // Open a dialog here @@ -283,7 +287,7 @@ describe('FileListComponent', () => { let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); when(() => setCategoryMock(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -307,7 +311,7 @@ describe('FileListComponent', () => { let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); when(() => setCategoryMock(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -334,7 +338,7 @@ describe('FileListComponent', () => { let findOrCreateFolderMock = MockInstance(FileService, 'findOrCreateFolder', mock()); when(() => findOrCreateFolderMock('Cat45', 'baseFolderId')).thenReturn(of('parentCat45Id')); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -361,7 +365,7 @@ describe('FileListComponent', () => { when(() => findOrCreateFolderMock('ParentCat8', 'baseFolderId')).thenReturn(of('parentCat8Id')); when(() => findOrCreateFolderMock('Cat7', 'parentCat8Id')).thenReturn(of('cat7Id')); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -375,7 +379,7 @@ describe('FileListComponent', () => { // No failure from mock setup })) - it('should suggest root categories', async () => { + it('should suggest root categories', fakeAsync(async () => { // Arrange let cat1Folder = mockFolderElement('cat1'); let cat2Folder = mockFolderElement('cat2'); @@ -383,7 +387,7 @@ describe('FileListComponent', () => { let fileElement1 = mockFileElement('name1'); mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -393,9 +397,9 @@ describe('FileListComponent', () => { // Assert let expected = await page.getSuggestedCategoryInDialog(); expect(expected).toEqual(['cat1', 'cat2']) - }) + })) - it('should suggest root categories and filter them by the current input', async () => { + it('should suggest root categories and filter them by the current input', fakeAsync(async () => { // Arrange let cat1Folder = mockFolderElement('cat1'); let cat2Folder = mockFolderElement('cat2'); @@ -403,7 +407,7 @@ describe('FileListComponent', () => { let fileElement1 = mockFileElement('name1'); mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -414,9 +418,9 @@ describe('FileListComponent', () => { // Assert let expected = await page.getSuggestedCategoryInDialog(); expect(expected).toEqual(['cat1']) - }) + })) - it('should be able to select a suggested category', async () => { + it('should be able to select a suggested category', fakeAsync(async () => { // Arrange let cat1Folder = mockFolderElement('cat1'); let cat2Folder = mockFolderElement('cat2'); @@ -433,7 +437,7 @@ describe('FileListComponent', () => { let findOrCreateFolderMock = MockInstance(FileService, 'findOrCreateFolder', mock()); when(() => findOrCreateFolderMock(cat1Folder.name, 'baseFolderId')).thenReturn(of(cat1Folder.id)); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -445,9 +449,9 @@ describe('FileListComponent', () => { // Assert // No failure from mock setup - }) + })) - it('should suggest sub-categories', async () => { + it('should suggest sub-categories', fakeAsync(async () => { // Arrange let cat1Folder = mockFolderElement('cat1'); let cat2Folder = mockFolderElement('cat2'); @@ -455,7 +459,7 @@ describe('FileListComponent', () => { let fileElement1 = mockFileElement('name1'); mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -467,7 +471,7 @@ describe('FileListComponent', () => { // Assert let expected = await page.getSuggestedCategoryInDialog(); expect(expected).toEqual(['cat1b']) - }) + })) it('should clear category input after selecting a category', fakeAsync(async () => { // Arrange @@ -475,7 +479,7 @@ describe('FileListComponent', () => { let fileElement1 = mockFileElement('name1'); mockListItemsAndCategories([cat1Folder, fileElement1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges() let page = new Page(fixture); // Act @@ -495,7 +499,7 @@ describe('FileListComponent', () => { let fileElement = mockFileElement('name1'); mockListItemsAndCategories([cat1Folder, cat1bFolder, fileElement]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -512,14 +516,14 @@ describe('FileListComponent', () => { expect(result).toEqual(['cat1']) })) - it('should initialize the category with the existing one', async () => { + it('should initialize the category with the existing one', fakeAsync(async () => { // Arrange let cat1Folder = mockFolderElement('cat1'); let cat1bFolder = mockFolderElement('cat1b', cat1Folder.id); let fileElement1 = mockFileElement('name1', cat1bFolder.id); mockListItemsAndCategories([cat1Folder, cat1bFolder, fileElement1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -529,7 +533,7 @@ describe('FileListComponent', () => { // Assert let result = await page.getCategoriesInDialog(); expect(result).toEqual(['cat1', 'cat1b']) - }) + })) it('When moving the last file from a category, should also remove the category', fakeAsync(async () => { // Arrange @@ -548,7 +552,7 @@ describe('FileListComponent', () => { // After removing the category, we expect another refresh when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([fileElementAfterRefresh], trashObservable)); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -624,10 +628,10 @@ describe('FileListComponent', () => { }) describe('Filter by file category', () => { - it('should filter out one item out of two items', () => { + it('should filter out one item out of two items', fakeAsync(() => { // Arrange mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Act Page.selectCategoryFilter('Cat1Child'); @@ -637,13 +641,13 @@ describe('FileListComponent', () => { let actionsRow = 'more_vert'; let expected = [['name2', 'Cat1Cat1Child', 'Aug 3, 2023, 2:54:55 PM', '1.75 kB', actionsRow]]; expect(Page.getTableRows()).toEqual(expected); - }) + })) - it('should filter based on root category', () => { + it('should filter based on root category', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Act Page.selectCategoryFilter('Image'); @@ -651,13 +655,13 @@ describe('FileListComponent', () => { // Assert fixture.detectChanges(); expect(Page.getDisplayedFileNames()).toEqual(['avatar.png', 'default.png', 'funny.png']) - }) + })) - it('should filter on two unrelated categories', () => { + it('should filter on two unrelated categories', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Act Page.selectCategoryFilter('TXT'); @@ -666,13 +670,13 @@ describe('FileListComponent', () => { // Assert fixture.detectChanges() expect(Page.getDisplayedFileNames()).toEqual(['avatar.png', 'text.txt']) - }) + })) - it('should allow removing a category filter', () => { + it('should allow removing a category filter', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Act Page.selectCategoryFilter('TXT'); @@ -681,13 +685,13 @@ describe('FileListComponent', () => { // Assert fixture.detectChanges() expect(Page.getDisplayedFileNames()).toEqual(['avatar.png', 'default.png', 'funny.png', 'text.txt']) - }) + })) - it('should allow filtering on the file categories, from a row of the table list', () => { + it('should allow filtering on the file categories, from a row of the table list', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges() // Act Page.selectCategoryFilterOnFileRow('avatar.png', 'Avatar'); @@ -695,13 +699,13 @@ describe('FileListComponent', () => { // Assert fixture.detectChanges() expect(Page.getDisplayedFileNames()).toEqual(['avatar.png']) - }) + })) - it('when filtering from a row, should change the filter state on the categories list (for leaf category)', () => { + it('when filtering from a row, should change the filter state on the categories list (for leaf category)', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Act Page.selectCategoryFilterOnFileRow('avatar.png', 'Avatar'); @@ -709,27 +713,28 @@ describe('FileListComponent', () => { // Assert fixture.detectChanges() expect(Page.isCategorySelectedOnCategoriesList('Avatar')).toBeTruthy(); - }) + })) - it('when filtering from a row, should change the filter state on the categories list (for parent category)', () => { + it('when filtering from a row, should change the filter state on the categories list (for parent category)', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Act Page.selectCategoryFilterOnFileRow('avatar.png', 'Image'); // Assert + tick(); fixture.detectChanges() expect(Page.isCategorySelectedOnCategoriesList('Image')).toBeTruthy(); - }) + })) - it('when filtering from the categories list, should change the filter state on the rows', () => { + it('when filtering from the categories list, should change the filter state on the rows', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); // Act Page.selectCategoryFilter('TXT'); @@ -737,19 +742,19 @@ describe('FileListComponent', () => { // Assert fixture.detectChanges() expect(Page.isCategorySelectedOnFileRow('text.txt', 'TXT')).toBeTruthy(); - }) + })) - it('in categories list, show a expand icon on parent category only', () => { + it('in categories list, show a expand icon on parent category only', fakeAsync(() => { // Arrange mockTxtAndImageFiles(); // Act - MockRender(FileListComponent); + mockRenderAndWaitForChanges(); // Assert expect(Page.isCategoryWithExpandIcon('TXT')).toBeFalsy(); expect(Page.isCategoryWithExpandIcon('Image')).toBeTruthy(); - }) + })) }) }); @@ -782,10 +787,7 @@ function mockFolderElement(name: string, parentId: string = 'baseFolderId', id: } function mockListItemsAndCategories(itemsAndCategories: (FileElement | FolderElement)[]) { - let findOrCreateBaseFolderMock = MockInstance(BaseFolderService, 'findOrCreateBaseFolder', - mock()); - - when(() => findOrCreateBaseFolderMock()).thenReturn(of('baseFolderId')); + mockBaseFolderService(); let fileServiceMock = mockFileService(); when(() => fileServiceMock.findAll()).thenReturn(of(itemsAndCategories)); return fileServiceMock; diff --git a/src/app/file-upload/base-folder.service.spec.ts b/src/app/file-upload/base-folder.service.spec.ts index 3ca81bc..bfa0508 100644 --- a/src/app/file-upload/base-folder.service.spec.ts +++ b/src/app/file-upload/base-folder.service.spec.ts @@ -1,10 +1,11 @@ import {BaseFolderService} from './base-folder.service'; -import {MockBuilder, MockRender} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {fakeAsync, tick} from "@angular/core/testing"; -import {when} from "strong-mock"; +import {mock, when} from "strong-mock"; import {of} from "rxjs"; import {mockFileService} from "../file-list/file.service.spec"; +import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; describe('BaseFolderService', () => { beforeEach(() => MockBuilder(BaseFolderService, AppModule)); @@ -35,3 +36,15 @@ describe('BaseFolderService', () => { expect(result).toBe('folderId51'); })); }); + +export function mockBaseFolderService() { + let baseFolderService = mock(); + MockInstance(BaseFolderService, () => { + return { + findOrCreateBaseFolder: baseFolderService.findOrCreateBaseFolder + } + }); + when(() => baseFolderService.findOrCreateBaseFolder()) + .thenReturn(mustBeConsumedAsyncObservable('baseFolderId')); + return baseFolderService; +} diff --git a/src/app/file-upload/file-upload.service.spec.ts b/src/app/file-upload/file-upload.service.spec.ts index 6808d60..60010fe 100644 --- a/src/app/file-upload/file-upload.service.spec.ts +++ b/src/app/file-upload/file-upload.service.spec.ts @@ -4,7 +4,7 @@ import {AppModule} from "../app.module"; import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; import {HttpClientModule, HttpEventType, HttpProgressEvent, HttpSentEvent} from "@angular/common/http"; import {fakeAsync, TestBed, tick} from "@angular/core/testing"; -import {mockFindOrCreateBaseFolder} from "../../testing/common-testing-function.spec"; +import {mockBaseFolderService} from "./base-folder.service.spec"; describe('FileUploadService', () => { beforeEach(() => @@ -23,7 +23,7 @@ describe('FileUploadService', () => { it('should upload', fakeAsync(() => { // Arrange let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); - mockFindOrCreateBaseFolder(); + mockBaseFolderService() const service = MockRender(FileUploadService).point.componentInstance; let httpTestingController = TestBed.inject(HttpTestingController); @@ -39,7 +39,7 @@ describe('FileUploadService', () => { expect(req.request.method).toEqual('POST'); expect(req.request.body).toEqual({ name: 'test.txt', - parents: ['parentId7854'], + parents: ['baseFolderId'], mimeType: 'application/txt', 'Content-Type': 'application/txt', 'Content-Length': 12 @@ -64,7 +64,7 @@ describe('FileUploadService', () => { it('should filter out unwanted http events when uploading', fakeAsync(() => { // Arrange let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); - mockFindOrCreateBaseFolder(); + mockBaseFolderService(); const service = MockRender(FileUploadService).point.componentInstance; let httpTestingController = TestBed.inject(HttpTestingController); @@ -85,7 +85,7 @@ describe('FileUploadService', () => { expect(req.request.method).toEqual('POST'); expect(req.request.body).toEqual({ name: 'test.txt', - parents: ['parentId7854'], + parents: ['baseFolderId'], mimeType: 'application/txt', 'Content-Type': 'application/txt', 'Content-Length': 12 diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 7a1cf69..5141d7d 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -1,12 +1,22 @@ import {RuleService} from './rule.service'; -import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; +import {MockBuilder, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {mockFileService} from "../file-list/file.service.spec"; -import {mock, when} from "strong-mock"; +import {when} from "strong-mock"; import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {fakeAsync, tick} from "@angular/core/testing"; import {mockFileElement} from "../file-list/file-list.component.spec"; -import {BaseFolderService} from "../file-upload/base-folder.service"; +import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; +import {FileService} from "../file-list/file.service"; + + +function mockBillCategoryFindOrCreate(fileService: FileService) { + when(() => fileService.findOrCreateFolder("Electricity", "baseFolderId")) + .thenReturn(mustBeConsumedAsyncObservable('elecCatId548')); + + when(() => fileService.findOrCreateFolder("Bills", "elecCatId548")) + .thenReturn(mustBeConsumedAsyncObservable('billsCatId489')); +} describe('RuleService', () => { beforeEach(() => MockBuilder(RuleService, AppModule)); @@ -22,27 +32,14 @@ describe('RuleService', () => { describe('runAll', () => { it('should automatically categorize a file', fakeAsync(() => { // Arrange - let baseFolderService = mock(); - MockInstance(BaseFolderService, () => { - return { - findOrCreateBaseFolder: baseFolderService.findOrCreateBaseFolder - } - }); - when(() => baseFolderService.findOrCreateBaseFolder()) - .thenReturn(mustBeConsumedAsyncObservable('baseFolderId')); + mockBaseFolderService(); let fileService = mockFileService(); - - when(() => fileService.findOrCreateFolder("Electricity", "baseFolderId")) - .thenReturn(mustBeConsumedAsyncObservable('elecCatId548')); - - when(() => fileService.findOrCreateFolder("Bills", "elecCatId548")) - .thenReturn(mustBeConsumedAsyncObservable('billsCatId489')); + mockBillCategoryFindOrCreate(fileService); let file = mockFileElement('electricity_bill.pdf'); when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([file])) - // The file should be set to the bills category when(() => fileService.setCategory(file.id, 'billsCatId489')) .thenReturn(mustBeConsumedAsyncObservable(undefined)); @@ -56,5 +53,27 @@ describe('RuleService', () => { tick(); // No failure in mock setup })); + + it('should not categorize a file which is already in the correct category', fakeAsync(() => { + // Arrange + mockBaseFolderService(); + + let fileService = mockFileService(); + + mockBillCategoryFindOrCreate(fileService); + + let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); + when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([file])) + + + const service = MockRender(RuleService).point.componentInstance; + + // Act + service.runAll().subscribe(); + + // Assert + tick(); + // No unexpected calls to fileService.setCategory + })); }) }); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 6d0596f..bb08868 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {Rule, SAMPLE_RULES} from "./rules.component"; import {FileService} from "../file-list/file.service"; -import {map, mergeMap, Observable, of, zip} from "rxjs"; +import {filter, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {BaseFolderService} from "../file-upload/base-folder.service"; @@ -13,7 +13,13 @@ export class RuleService { } runAll(): Observable { - let rules = SAMPLE_RULES; + let rules = SAMPLE_RULES + .map(rule => { + let copy = {...rule}; + copy.category = Object.assign([], rule.category); + return copy; + }); + return this.fileService.findAll() .pipe(mergeMap(fileOrFolders => { // Get all files @@ -61,9 +67,11 @@ export class RuleService { fileToCategoryMap .forEach((category, file) => { categoryRequests.push(this.findOrCreateCategories(category, baseFolderId) - .pipe(mergeMap(categoryId => { - return this.fileService.setCategory(file.id, categoryId); - }))); + // There is no need to set the category if the current category is correct + .pipe(filter(categoryId => file.parentId !== categoryId), + mergeMap(categoryId => { + return this.fileService.setCategory(file.id, categoryId); + }))); }); let observable = zip(categoryRequests); return observable diff --git a/src/testing/common-testing-function.spec.ts b/src/testing/common-testing-function.spec.ts index 94cc7d3..a164f02 100644 --- a/src/testing/common-testing-function.spec.ts +++ b/src/testing/common-testing-function.spec.ts @@ -1,13 +1,4 @@ -import {mock, when} from "strong-mock"; -import {BaseFolderService} from "../app/file-upload/base-folder.service"; -import {Observable, of, Subscriber, TeardownLogic} from "rxjs"; -import {MockInstance} from "ng-mocks"; - -export function mockFindOrCreateBaseFolder() { - let findOrCreateBaseFolderMock = MockInstance(BaseFolderService, 'findOrCreateBaseFolder', - mock()) - when(() => findOrCreateBaseFolderMock()).thenReturn(of('parentId7854')); -} +import {Observable, Subscriber, TeardownLogic} from "rxjs"; export async function findAsyncSequential( array: T[], From 5a51a43c4abde28a6a335d2f62f7e00d758a9c48 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 11:27:46 +0100 Subject: [PATCH 10/66] [us40] Add form to create a new rule --- src/app/rules/rule.repository.spec.ts | 16 ++++ src/app/rules/rule.repository.ts | 10 +++ src/app/rules/rule.service.spec.ts | 15 +++- src/app/rules/rule.service.ts | 4 + src/app/rules/rules.component.html | 40 +++++++++- src/app/rules/rules.component.scss | 22 ++++++ src/app/rules/rules.component.spec.ts | 102 +++++++++++++++++++++++--- src/app/rules/rules.component.ts | 19 +++++ 8 files changed, 212 insertions(+), 16 deletions(-) create mode 100644 src/app/rules/rule.repository.spec.ts create mode 100644 src/app/rules/rule.repository.ts diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts new file mode 100644 index 0000000..65b3949 --- /dev/null +++ b/src/app/rules/rule.repository.spec.ts @@ -0,0 +1,16 @@ +import {RuleRepository} from "./rule.repository"; +import {MockBuilder, MockRender} from "ng-mocks"; +import {AppModule} from "../app.module"; + + +describe('RuleRepository', () => { + beforeEach(() => MockBuilder(RuleRepository, AppModule)); + + it('should be created', () => { + // Act + const ruleRepository = MockRender(RuleRepository).point.componentInstance; + + // Assert + expect(ruleRepository).toBeTruthy(); + }); +}); diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts new file mode 100644 index 0000000..35a4c1f --- /dev/null +++ b/src/app/rules/rule.repository.ts @@ -0,0 +1,10 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class RuleRepository { + + constructor() { + } +} diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 5141d7d..218380f 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -1,8 +1,8 @@ import {RuleService} from './rule.service'; -import {MockBuilder, MockRender} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {mockFileService} from "../file-list/file.service.spec"; -import {when} from "strong-mock"; +import {mock, when} from "strong-mock"; import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {fakeAsync, tick} from "@angular/core/testing"; import {mockFileElement} from "../file-list/file-list.component.spec"; @@ -77,3 +77,14 @@ describe('RuleService', () => { })); }) }); + +export function mockRuleService() { + let ruleService = mock(); + MockInstance(RuleService, () => { + return { + runAll: ruleService.runAll, + create: ruleService.create + } + }); + return ruleService; +} diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index bb08868..e2cf27b 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -91,4 +91,8 @@ export class RuleService { } return of(categoryId); } + + create(rule: Rule) { + return undefined; + } } diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 4b30d57..2f48b27 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -1,11 +1,47 @@
Go back -   -

Setup rules

+
+ +   + +
+
+
+ + Name + + +   + + Category + + + {{cat}} + + + + + +
+ + Script + + +
+ +   + +
+
diff --git a/src/app/rules/rules.component.scss b/src/app/rules/rules.component.scss index c581f24..09e9db2 100644 --- a/src/app/rules/rules.component.scss +++ b/src/app/rules/rules.component.scss @@ -6,6 +6,28 @@ padding: 20px; } +.actionButtons { + padding: 20px 0; +} + .scriptFormField { width: 100%; } + +form { + width: 100%; + max-width: 800px; +} + +.nameAndCategoryFields { + width: 100%; + display: flex; +} + +mat-form-field { + flex-grow: 1; +} + +.fullWidth { + width: 100%; +} diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 48cd7e8..94b0cdc 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -1,16 +1,30 @@ -import {RulesComponent} from './rules.component'; -import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; +import {Rule, RulesComponent} from './rules.component'; +import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; -import {RuleService} from "./rule.service"; -import {mock, when} from "strong-mock"; +import {when} from "strong-mock"; import {MatButtonHarness} from "@angular/material/button/testing"; -import {HarnessLoader} from "@angular/cdk/testing"; +import {HarnessLoader, TestKey} from "@angular/cdk/testing"; import {ComponentFixture} from "@angular/core/testing"; import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; +import {mockRuleService} from "./rule.service.spec"; +import {MatInputHarness} from "@angular/material/input/testing"; +import {MatFormFieldHarness} from "@angular/material/form-field/testing"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; +import {MatInputModule} from "@angular/material/input"; +import {FormsModule} from "@angular/forms"; +import {MatChipsModule} from "@angular/material/chips"; +import {MatChipGridHarness} from "@angular/material/chips/testing"; describe('RulesComponent', () => { - beforeEach(() => MockBuilder(RulesComponent, AppModule)); + beforeEach(() => MockBuilder(RulesComponent, AppModule) + .keep(MatInputModule) + .keep(MatFormFieldModule) + .keep(FormsModule) + .keep(MatChipsModule) + .replace(BrowserAnimationsModule, NoopAnimationsModule) + ); it('should create', () => { let component = MockRender(RulesComponent).point.componentInstance; @@ -29,12 +43,7 @@ describe('RulesComponent', () => { it('should run all the rules when clicking on "run rules" button', async () => { // Arrange - let ruleService = mock(); - MockInstance(RuleService, () => { - return { - runAll: ruleService.runAll - } - }); + let ruleService = mockRuleService(); when(() => ruleService.runAll()).thenReturn(mustBeConsumedAsyncObservable(undefined)); let fixture = MockRender(RulesComponent); let page = new Page(fixture); @@ -45,6 +54,33 @@ describe('RulesComponent', () => { // Assert // no failure from mock setup }) + + it('should create a new rule', async () => { + // Arrange + let ruleService = mockRuleService(); + + let expectedRule: Rule = { + name: 'New rule', + category: ['Cat1', 'ChildCat1'], + script: 'return fileName === "child_cat_1.txt"' + }; + when(() => ruleService.create(expectedRule)).thenReturn(undefined); + + let fixture = MockRender(RulesComponent); + + let page = new Page(fixture); + + // Act + await page.clickOnCreateNewRule(); + fixture.detectChanges(); + await page.setCreateRuleName('New rule'); + await page.setCreateRuleCategory(['Cat1', 'ChildCat1']); + await page.setCreateRuleScript('return fileName === "child_cat_1.txt"'); + await page.clickOnCreate(); + + // Assert + // No failure in mock setup + }) }); class Page { @@ -79,4 +115,46 @@ class Page { let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Run all'})); return button.click(); } + + async clickOnCreateNewRule() { + let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Create new rule'})); + await button.click(); + } + + async setCreateRuleName(name: string) { + let input = await this.getInputByFloatingLabel('Name'); + await input.setValue(name); + } + + async setCreateRuleCategory(category: string[]) { + // let inputHarness = await this.loader.getHarness(MatInputHarness.with({placeholder: 'Select category...'})); + let chipGridHarness = await this.loader.getHarness(MatChipGridHarness); + let inputHarness = await chipGridHarness.getInput() + if (inputHarness) { + for (const catElement of category) { + await inputHarness.setValue(catElement); + let testElement = await inputHarness.host(); + await testElement.sendKeys(TestKey.ENTER) + } + } + } + + async setCreateRuleScript(script: string) { + let inputHarness = await this.getInputByFloatingLabel('Script'); + await inputHarness.setValue(script); + } + + async clickOnCreate() { + let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Create'})); + await button.click(); + } + + private async getInputByFloatingLabel(floatingLabelText: string | RegExp) { + let formFieldHarness = await this.loader.getHarness(MatFormFieldHarness.with({floatingLabelText: floatingLabelText})); + let control = await formFieldHarness.getControl(); + if (control) { + return control as MatInputHarness; + } + throw Error("No input found with floating label '" + floatingLabelText + "'"); + } } diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 073a699..2d05580 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -1,5 +1,7 @@ import {Component} from '@angular/core'; import {RuleService} from "./rule.service"; +import {ENTER} from "@angular/cdk/keycodes"; +import {MatChipInputEvent} from "@angular/material/chips"; export interface Rule { name: string; @@ -23,7 +25,15 @@ export const SAMPLE_RULES: Rule[] = [{ styleUrls: ['./rules.component.scss'] }) export class RulesComponent { + readonly separatorKeysCodes = [ENTER] as const; + rules = SAMPLE_RULES; + showCreate: boolean = false; + ruleToCreate: Rule = { + name: '', + category: [], + script: '' + }; constructor(private ruleService: RuleService) { } @@ -31,4 +41,13 @@ export class RulesComponent { runAll() { this.ruleService.runAll().subscribe(); } + + add(event: MatChipInputEvent) { + this.ruleToCreate.category.push(event.value); + event.chipInput.clear(); + } + + createNewRule() { + this.ruleService.create(this.ruleToCreate); + } } From fb32a333ae100f3c87ce332bce18b373706c6724 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 11:47:38 +0100 Subject: [PATCH 11/66] [us40] Persist rules on creation --- src/app/database/db.ts | 15 +++++++++++++ src/app/rules/rule.repository.spec.ts | 31 ++++++++++++++++++++++++++- src/app/rules/rule.repository.ts | 12 +++++++++++ src/app/rules/rule.service.ts | 13 +++++------ src/app/rules/rules.component.spec.ts | 3 ++- src/app/rules/rules.component.ts | 6 +----- 6 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 src/app/database/db.ts diff --git a/src/app/database/db.ts b/src/app/database/db.ts new file mode 100644 index 0000000..f048214 --- /dev/null +++ b/src/app/database/db.ts @@ -0,0 +1,15 @@ +import Dexie, {Table} from 'dexie'; +import {Rule} from "../rules/rule.repository"; + +export class AppDB extends Dexie { + rules!: Table; + + constructor() { + super('ngdexieliveQuery'); + this.version(3).stores({ + rules: '++id', + }); + } +} + +export const db = new AppDB(); diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index 65b3949..769591a 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -1,11 +1,15 @@ -import {RuleRepository} from "./rule.repository"; +import {Rule, RuleRepository} from "./rule.repository"; import {MockBuilder, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; +import {db} from "../database/db"; describe('RuleRepository', () => { beforeEach(() => MockBuilder(RuleRepository, AppModule)); + // Db cleanup after each test + afterEach(() => db.delete()); + it('should be created', () => { // Act const ruleRepository = MockRender(RuleRepository).point.componentInstance; @@ -13,4 +17,29 @@ describe('RuleRepository', () => { // Assert expect(ruleRepository).toBeTruthy(); }); + + describe('create', () => { + it('should persist a new rule', async () => { + // Arrange + const ruleRepository = MockRender(RuleRepository).point.componentInstance; + let rule: Rule = { + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }; + + // Act + ruleRepository.create(rule) + + // Assert + let rules = await db.rules.toArray(); + expect(rules) + .toEqual([{ + id: 1, + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }]); + }) + }) }); diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 35a4c1f..3292367 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -1,4 +1,12 @@ import {Injectable} from '@angular/core'; +import {db} from "../database/db"; + +export interface Rule { + id?: number; + name: string; + category: string[]; + script: string; +} @Injectable({ providedIn: 'root' @@ -7,4 +15,8 @@ export class RuleRepository { constructor() { } + + create(rule: Rule) { + db.rules.add(rule); + } } diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index e2cf27b..742812e 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,15 +1,16 @@ import {Injectable} from '@angular/core'; -import {Rule, SAMPLE_RULES} from "./rules.component"; +import {SAMPLE_RULES} from "./rules.component"; import {FileService} from "../file-list/file.service"; import {filter, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {BaseFolderService} from "../file-upload/base-folder.service"; +import {Rule, RuleRepository} from "./rule.repository"; @Injectable({ providedIn: 'root' }) export class RuleService { - constructor(private fileService: FileService, private baseFolderService: BaseFolderService) { + constructor(private fileService: FileService, private baseFolderService: BaseFolderService, private ruleRepository: RuleRepository) { } runAll(): Observable { @@ -34,6 +35,10 @@ export class RuleService { })); } + create(rule: Rule) { + return this.ruleRepository.create(rule); + } + /** * Run the given rules on the given files and return the associated category for each file that got a matching rule */ @@ -91,8 +96,4 @@ export class RuleService { } return of(categoryId); } - - create(rule: Rule) { - return undefined; - } } diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 94b0cdc..47cf5c0 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -1,4 +1,4 @@ -import {Rule, RulesComponent} from './rules.component'; +import {RulesComponent} from './rules.component'; import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; import {when} from "strong-mock"; @@ -16,6 +16,7 @@ import {MatInputModule} from "@angular/material/input"; import {FormsModule} from "@angular/forms"; import {MatChipsModule} from "@angular/material/chips"; import {MatChipGridHarness} from "@angular/material/chips/testing"; +import {Rule} from "./rule.repository"; describe('RulesComponent', () => { beforeEach(() => MockBuilder(RulesComponent, AppModule) diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 2d05580..a92023f 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -2,12 +2,8 @@ import {Component} from '@angular/core'; import {RuleService} from "./rule.service"; import {ENTER} from "@angular/cdk/keycodes"; import {MatChipInputEvent} from "@angular/material/chips"; +import {Rule} from "./rule.repository"; -export interface Rule { - name: string; - category: string[]; - script: string; -} export const SAMPLE_RULES: Rule[] = [{ name: 'Electric bill', From 336b75a01ddee55137b6d8a414cc1d5bf9fb612d Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 13:26:19 +0100 Subject: [PATCH 12/66] [us40] List persisted rules instead of hardcoded ones --- src/app/rules/rule.repository.spec.ts | 49 ++++++++++++++++++++++++++- src/app/rules/rule.repository.ts | 4 +++ src/app/rules/rule.service.spec.ts | 22 +++++++++--- src/app/rules/rule.service.ts | 35 ++++++++++--------- src/app/rules/rules.component.spec.ts | 39 ++++++++++++++++++--- src/app/rules/rules.component.ts | 16 +++------ 6 files changed, 127 insertions(+), 38 deletions(-) diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index 769591a..cfa7bfa 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -1,7 +1,8 @@ import {Rule, RuleRepository} from "./rule.repository"; -import {MockBuilder, MockRender} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {db} from "../database/db"; +import {mock} from "strong-mock"; describe('RuleRepository', () => { @@ -18,6 +19,42 @@ describe('RuleRepository', () => { expect(ruleRepository).toBeTruthy(); }); + describe('findAll', () => { + it('should list two rules', async () => { + // Arrange + const ruleRepository = MockRender(RuleRepository).point.componentInstance; + let rule1: Rule = { + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }; + ruleRepository.create(rule1) + let rule2: Rule = { + name: 'TestRule2', + category: ['Test2', 'ChildTest2'], + script: 'return false' + }; + ruleRepository.create(rule2) + + // Act + let result = await ruleRepository.findAll(); + + // Assert + expect(result) + .toEqual([{ + id: 1, + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }, { + id: 2, + name: 'TestRule2', + category: ['Test2', 'ChildTest2'], + script: 'return false' + }]); + }); + }); + describe('create', () => { it('should persist a new rule', async () => { // Arrange @@ -43,3 +80,13 @@ describe('RuleRepository', () => { }) }) }); + +export function mockRuleRepository() { + let ruleRepositoryMock = mock(); + MockInstance(RuleRepository, (instance, injector) => { + return { + findAll: ruleRepositoryMock.findAll + } + }); + return ruleRepositoryMock; +} diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 3292367..92e29d7 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -19,4 +19,8 @@ export class RuleRepository { create(rule: Rule) { db.rules.add(rule); } + + findAll(): Promise { + return db.rules.toArray(); + } } diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 218380f..e31e0ae 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -8,6 +8,8 @@ import {fakeAsync, tick} from "@angular/core/testing"; import {mockFileElement} from "../file-list/file-list.component.spec"; import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; import {FileService} from "../file-list/file.service"; +import {mockRuleRepository} from "./rule.repository.spec"; +import {getSampleRules} from "./rules.component.spec"; function mockBillCategoryFindOrCreate(fileService: FileService) { @@ -37,6 +39,10 @@ describe('RuleService', () => { let fileService = mockFileService(); mockBillCategoryFindOrCreate(fileService); + let ruleRepository = mockRuleRepository(); + when(() => ruleRepository.findAll()) + .thenResolve(getSampleRules()); + let file = mockFileElement('electricity_bill.pdf'); when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([file])) @@ -65,6 +71,9 @@ describe('RuleService', () => { let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([file])) + let ruleRepository = mockRuleRepository(); + when(() => ruleRepository.findAll()) + .thenResolve(getSampleRules()); const service = MockRender(RuleService).point.componentInstance; @@ -78,13 +87,18 @@ describe('RuleService', () => { }) }); +let ruleServiceMock: RuleService; + export function mockRuleService() { - let ruleService = mock(); + if (!ruleServiceMock) { + ruleServiceMock = mock(); + } MockInstance(RuleService, () => { return { - runAll: ruleService.runAll, - create: ruleService.create + runAll: ruleServiceMock.runAll, + create: ruleServiceMock.create, + findAll: ruleServiceMock.findAll } }); - return ruleService; + return ruleServiceMock; } diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 742812e..200e09a 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,7 +1,6 @@ import {Injectable} from '@angular/core'; -import {SAMPLE_RULES} from "./rules.component"; import {FileService} from "../file-list/file.service"; -import {filter, map, mergeMap, Observable, of, zip} from "rxjs"; +import {filter, from, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {BaseFolderService} from "../file-upload/base-folder.service"; import {Rule, RuleRepository} from "./rule.repository"; @@ -14,24 +13,20 @@ export class RuleService { } runAll(): Observable { - let rules = SAMPLE_RULES - .map(rule => { - let copy = {...rule}; - copy.category = Object.assign([], rule.category); - return copy; - }); + return from(this.ruleRepository.findAll()) + .pipe(mergeMap(rules => { + return this.fileService.findAll() + .pipe(mergeMap(fileOrFolders => { + // Get all files + let files = fileOrFolders.filter(file => isFileElement(file)) + .map(value => value as FileElement); - return this.fileService.findAll() - .pipe(mergeMap(fileOrFolders => { - // Get all files - let files = fileOrFolders.filter(file => isFileElement(file)) - .map(value => value as FileElement); + // Run the script for each file to get the associated category + let fileToCategoryMap = this.computeFileToCategoryMap(files, rules); - // Run the script for each file to get the associated category - let fileToCategoryMap = this.computeFileToCategoryMap(files, rules); - - // Set the computed category for each files - return this.setAllFileCategory(fileToCategoryMap); + // Set the computed category for each files + return this.setAllFileCategory(fileToCategoryMap); + })) })); } @@ -39,6 +34,10 @@ export class RuleService { return this.ruleRepository.create(rule); } + findAll(): Promise { + return this.ruleRepository.findAll(); + } + /** * Run the given rules on the given files and return the associated category for each file that got a matching rule */ diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 47cf5c0..2feec9c 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -4,7 +4,7 @@ import {AppModule} from "../app.module"; import {when} from "strong-mock"; import {MatButtonHarness} from "@angular/material/button/testing"; import {HarnessLoader, TestKey} from "@angular/cdk/testing"; -import {ComponentFixture} from "@angular/core/testing"; +import {ComponentFixture, fakeAsync, tick} from "@angular/core/testing"; import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {mockRuleService} from "./rule.service.spec"; @@ -28,22 +28,34 @@ describe('RulesComponent', () => { ); it('should create', () => { + // Arrange + mockSampleRules() + + // Act let component = MockRender(RulesComponent).point.componentInstance; + + // Assert expect(component).toBeTruthy(); }); - it('should list two rules', () => { + it('should list two rules', fakeAsync(() => { + // Arrange + mockSampleRules(); + // Act - MockRender(RulesComponent); + let fixture = MockRender(RulesComponent); // Assert + tick(); + fixture.detectChanges(); expect(Page.getRuleNames()).toEqual(['Electric bill', 'Bank account statement']); expect(Page.getRuleCategory('Electric bill')).toEqual('Electricity  > Bills'); expect(Page.getRuleScript('Electric bill')).toEqual('return fileName === "electricity_bill.pdf"'); - }) + })) it('should run all the rules when clicking on "run rules" button', async () => { // Arrange + mockSampleRules(); let ruleService = mockRuleService(); when(() => ruleService.runAll()).thenReturn(mustBeConsumedAsyncObservable(undefined)); let fixture = MockRender(RulesComponent); @@ -59,6 +71,7 @@ describe('RulesComponent', () => { it('should create a new rule', async () => { // Arrange let ruleService = mockRuleService(); + mockSampleRules(); let expectedRule: Rule = { name: 'New rule', @@ -84,6 +97,24 @@ describe('RulesComponent', () => { }) }); + +function mockSampleRules() { + let ruleService = mockRuleService(); + when(() => ruleService.findAll()).thenResolve(getSampleRules()); +} + +export function getSampleRules(): Rule[] { + return [{ + name: 'Electric bill', + category: ['Electricity', 'Bills'], + script: 'return fileName === "electricity_bill.pdf"' + }, { + name: 'Bank account statement', + category: ['Bank', 'Account statement'], + script: 'return false' + }]; +} + class Page { private loader: HarnessLoader; diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index a92023f..7c0e49b 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -5,16 +5,6 @@ import {MatChipInputEvent} from "@angular/material/chips"; import {Rule} from "./rule.repository"; -export const SAMPLE_RULES: Rule[] = [{ - name: 'Electric bill', - category: ['Electricity', 'Bills'], - script: 'return fileName === "electricity_bill.pdf"' -}, { - name: 'Bank account statement', - category: ['Bank', 'Account statement'], - script: 'return false' -}]; - @Component({ selector: 'app-rules', templateUrl: './rules.component.html', @@ -23,7 +13,7 @@ export const SAMPLE_RULES: Rule[] = [{ export class RulesComponent { readonly separatorKeysCodes = [ENTER] as const; - rules = SAMPLE_RULES; + rules: Rule[] = []; showCreate: boolean = false; ruleToCreate: Rule = { name: '', @@ -32,6 +22,10 @@ export class RulesComponent { }; constructor(private ruleService: RuleService) { + ruleService.findAll() + .then(rules => { + this.rules = rules; + }) } runAll() { From 1a672e94ad5b359dd04d608a9cd1d993e1e08080 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 14:19:18 +0100 Subject: [PATCH 13/66] [us40] Refresh list after new rule creation --- src/app/database/db.ts | 5 +++++ src/app/rules/rule.repository.spec.ts | 12 ++++++++---- src/app/rules/rule.repository.ts | 4 ++-- src/app/rules/rules.component.spec.ts | 14 ++++++++++---- src/app/rules/rules.component.ts | 23 ++++++++++++++++++----- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/app/database/db.ts b/src/app/database/db.ts index f048214..b6cdfe4 100644 --- a/src/app/database/db.ts +++ b/src/app/database/db.ts @@ -6,6 +6,11 @@ export class AppDB extends Dexie { constructor() { super('ngdexieliveQuery'); + this.createSchema(); + } + + // Public for testing + createSchema() { this.version(3).stores({ rules: '++id', }); diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index cfa7bfa..156865f 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -9,7 +9,11 @@ describe('RuleRepository', () => { beforeEach(() => MockBuilder(RuleRepository, AppModule)); // Db cleanup after each test - afterEach(() => db.delete()); + afterEach(async () => { + await db.delete(); + db.createSchema(); + await db.open(); + }); it('should be created', () => { // Act @@ -28,13 +32,13 @@ describe('RuleRepository', () => { category: ['Test1', 'ChildTest1'], script: 'return true' }; - ruleRepository.create(rule1) + await ruleRepository.create(rule1) let rule2: Rule = { name: 'TestRule2', category: ['Test2', 'ChildTest2'], script: 'return false' }; - ruleRepository.create(rule2) + await ruleRepository.create(rule2) // Act let result = await ruleRepository.findAll(); @@ -66,7 +70,7 @@ describe('RuleRepository', () => { }; // Act - ruleRepository.create(rule) + await ruleRepository.create(rule) // Assert let rules = await db.rules.toArray(); diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 92e29d7..1566bc3 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -16,8 +16,8 @@ export class RuleRepository { constructor() { } - create(rule: Rule) { - db.rules.add(rule); + create(rule: Rule): Promise { + return db.rules.add(rule).then(); } findAll(): Promise { diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 2feec9c..5a60d12 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -68,18 +68,20 @@ describe('RulesComponent', () => { // no failure from mock setup }) - it('should create a new rule', async () => { + it('should create a new rule', fakeAsync(async () => { // Arrange let ruleService = mockRuleService(); - mockSampleRules(); + when(() => ruleService.findAll()).thenResolve([]); let expectedRule: Rule = { name: 'New rule', category: ['Cat1', 'ChildCat1'], script: 'return fileName === "child_cat_1.txt"' }; - when(() => ruleService.create(expectedRule)).thenReturn(undefined); + when(() => ruleService.create(expectedRule)).thenResolve(undefined); + // After refresh, there should be the new rule + when(() => ruleService.findAll()).thenResolve([expectedRule]); let fixture = MockRender(RulesComponent); let page = new Page(fixture); @@ -94,7 +96,11 @@ describe('RulesComponent', () => { // Assert // No failure in mock setup - }) + tick(); + fixture.detectChanges(); + expect(Page.getRuleNames()) + .toEqual(['New rule']); + })) }); diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 7c0e49b..44b7ccb 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -22,9 +22,19 @@ export class RulesComponent { }; constructor(private ruleService: RuleService) { - ruleService.findAll() - .then(rules => { - this.rules = rules; + this.refresh(); + } + + createNewRule() { + this.ruleService.create(this.ruleToCreate) + .then(() => { + this.showCreate = false; + this.ruleToCreate = { + name: '', + category: [], + script: '' + }; + this.refresh(); }) } @@ -37,7 +47,10 @@ export class RulesComponent { event.chipInput.clear(); } - createNewRule() { - this.ruleService.create(this.ruleToCreate); + private refresh() { + this.ruleService.findAll() + .then(rules => { + this.rules = rules; + }) } } From 57b1c85d8dd1fce1ec737a232f5473ee28cc2b4c Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 14:58:45 +0100 Subject: [PATCH 14/66] [us40] Add delete button to delete the existing rules --- src/app/rules/rule.repository.spec.ts | 20 +++++++++++++++ src/app/rules/rule.repository.ts | 7 +++++ src/app/rules/rule.service.spec.ts | 3 ++- src/app/rules/rule.service.ts | 4 +++ src/app/rules/rules.component.html | 1 + src/app/rules/rules.component.scss | 4 +++ src/app/rules/rules.component.spec.ts | 37 +++++++++++++++++++++++++++ src/app/rules/rules.component.ts | 7 +++++ 8 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index 156865f..80116d1 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -83,6 +83,26 @@ describe('RuleRepository', () => { }]); }) }) + + describe('delete', () => { + it('should delete one rule', async () => { + // Arrange + const ruleRepository = MockRender(RuleRepository).point.componentInstance; + let rule: Rule = { + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }; + await ruleRepository.create(rule) + + // Act + await ruleRepository.delete(rule); + + // Assert + let rules = await db.rules.toArray(); + expect(rules).toEqual([]); + }); + }); }); export function mockRuleRepository() { diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 1566bc3..051f13d 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -23,4 +23,11 @@ export class RuleRepository { findAll(): Promise { return db.rules.toArray(); } + + delete(rule: Rule): Promise { + if (rule.id) { + return db.rules.delete(rule.id); + } + return Promise.resolve(); + } } diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index e31e0ae..70a28d8 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -97,7 +97,8 @@ export function mockRuleService() { return { runAll: ruleServiceMock.runAll, create: ruleServiceMock.create, - findAll: ruleServiceMock.findAll + findAll: ruleServiceMock.findAll, + delete: ruleServiceMock.delete } }); return ruleServiceMock; diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 200e09a..9cecb07 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -95,4 +95,8 @@ export class RuleService { } return of(categoryId); } + + delete(rule: Rule) { + return this.ruleRepository.delete(rule); + } } diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 2f48b27..c779a64 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -55,6 +55,7 @@

Setup rules

{{rule.script}}
+
diff --git a/src/app/rules/rules.component.scss b/src/app/rules/rules.component.scss index 09e9db2..f6cf3ce 100644 --- a/src/app/rules/rules.component.scss +++ b/src/app/rules/rules.component.scss @@ -31,3 +31,7 @@ mat-form-field { .fullWidth { width: 100%; } + +.ruleDeleteButton { + margin-top: 20px; +} diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 5a60d12..aefe44e 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -101,6 +101,38 @@ describe('RulesComponent', () => { expect(Page.getRuleNames()) .toEqual(['New rule']); })) + + it('should delete an existing rule', fakeAsync(async () => { + // Arrange + let ruleService = mockRuleService(); + + let rule: Rule = { + name: 'Rule1', + category: ['Cat1', 'ChildCat1'], + script: 'return fileName === "child_cat_1.txt"' + }; + when(() => ruleService.findAll()).thenResolve([rule]); + + // A refresh is expected after delete + when(() => ruleService.findAll()).thenResolve([]); + + when(() => ruleService.delete(rule)).thenResolve(); + + let fixture = MockRender(RulesComponent); + tick(); + + let page = new Page(fixture); + + // Act + await page.deleteFirstRule(); + + // Assert + // No failure in mock setup + tick(); + fixture.detectChanges(); + expect(Page.getRuleNames()) + .toEqual([]); + })) }); @@ -195,4 +227,9 @@ class Page { } throw Error("No input found with floating label '" + floatingLabelText + "'"); } + + async deleteFirstRule() { + let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Delete'})); + await button.click(); + } } diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 44b7ccb..c9de5f9 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -53,4 +53,11 @@ export class RulesComponent { this.rules = rules; }) } + + delete(rule: Rule) { + this.ruleService.delete(rule) + .then(() => { + this.refresh(); + }) + } } From 9cad7c601c9024d8ffb05196673f66f5864c5712 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 16:58:33 +0100 Subject: [PATCH 15/66] [us40] Backup the database after creation or deletion of rules --- ...atabase-backup-and-restore.service.spec.ts | 48 +++++++++++++++++++ .../database-backup-and-restore.service.ts | 20 ++++++++ src/app/database/db.ts | 2 +- .../file-upload/file-upload.component.spec.ts | 23 ++++----- src/app/file-upload/file-upload.component.ts | 4 +- .../file-upload/file-upload.service.spec.ts | 19 ++++++-- src/app/file-upload/file-upload.service.ts | 29 +++++++---- src/app/rules/rule.repository.spec.ts | 10 +++- src/app/rules/rule.repository.ts | 14 +++--- 9 files changed, 132 insertions(+), 37 deletions(-) create mode 100644 src/app/database/database-backup-and-restore.service.spec.ts create mode 100644 src/app/database/database-backup-and-restore.service.ts diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts new file mode 100644 index 0000000..9400b75 --- /dev/null +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -0,0 +1,48 @@ +import {DatabaseBackupAndRestoreService} from './database-backup-and-restore.service'; +import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; +import {AppModule} from "../app.module"; +import {mockFileUploadService} from "../file-upload/file-upload.service.spec"; +import {It, mock, when} from "strong-mock"; +import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; +import {HttpEventType, HttpResponse} from "@angular/common/http"; + +describe('DatabaseBackupAndRestoreService', () => { + beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule)); + + it('should be created', () => { + // Act + const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + + // Assert + expect(databaseBackupAndRestoreService).toBeTruthy(); + }); + + describe('backup', () => { + it('should backup one rule', async () => { + // Arrange + let fileUploadService = mockFileUploadService(); + when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}))) + .thenReturn(mustBeConsumedAsyncObservable({ + type: HttpEventType.Response + } as HttpResponse)); + + const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + + // Act + await databaseBackupAndRestoreService.backup(); + + // Assert + // No failure in mock setup + }) + }) +}); + +export function mockDatabaseBackupAndRestoreService() { + let databaseBackupAndRestoreService = mock(); + MockInstance(DatabaseBackupAndRestoreService, () => { + return { + backup: databaseBackupAndRestoreService.backup + } + }); + return databaseBackupAndRestoreService; +} diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts new file mode 100644 index 0000000..f29f08c --- /dev/null +++ b/src/app/database/database-backup-and-restore.service.ts @@ -0,0 +1,20 @@ +import {Injectable} from '@angular/core'; +import {exportDB} from "dexie-export-import"; +import {db} from "./db"; +import {FileUploadService} from "../file-upload/file-upload.service"; +import {lastValueFrom} from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class DatabaseBackupAndRestoreService { + + constructor(private fileUploadService: FileUploadService) { + } + + async backup() { + let blob = await exportDB(db); + // TODO: Don't create the file when it already exists + await lastValueFrom(this.fileUploadService.upload({name: 'db.backup', blob})); + } +} diff --git a/src/app/database/db.ts b/src/app/database/db.ts index b6cdfe4..5edf9f6 100644 --- a/src/app/database/db.ts +++ b/src/app/database/db.ts @@ -5,7 +5,7 @@ export class AppDB extends Dexie { rules!: Table; constructor() { - super('ngdexieliveQuery'); + super('StoreMyDocsDB'); this.createSchema(); } diff --git a/src/app/file-upload/file-upload.component.spec.ts b/src/app/file-upload/file-upload.component.spec.ts index 4cef562..40a02ee 100644 --- a/src/app/file-upload/file-upload.component.spec.ts +++ b/src/app/file-upload/file-upload.component.spec.ts @@ -1,9 +1,9 @@ -import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ComponentFixture} from '@angular/core/testing'; import {FileUploadComponent} from './file-upload.component'; import {MatIconModule} from "@angular/material/icon"; import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; -import {FileUploadService} from "./file-upload.service"; +import {toFileOrBlob} from "./file-upload.service"; import {mock, when} from "strong-mock"; import {Observable, of} from "rxjs"; import {FileUploadElementComponent} from "./file-upload-element/file-upload-element.component"; @@ -12,6 +12,7 @@ import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; import {HarnessLoader} from "@angular/cdk/testing"; import {MatButtonHarness} from "@angular/material/button/testing"; import {GooglePickerService} from "./google-picker.service"; +import {mockFileUploadService} from "./file-upload.service.spec"; describe('FileUploadComponent', () => { beforeEach(() => { @@ -33,12 +34,10 @@ describe('FileUploadComponent', () => { // Arrange const fixture = MockRender(FileUploadComponent); const page = new Page(fixture); - let fileUploadService = TestBed.inject(FileUploadService); - let uploadMock = mock(); - fileUploadService.upload = uploadMock; + let fileUploadService = mockFileUploadService(); let file = new File([''], 'TestFile.txt'); - when(() => uploadMock(file)).thenReturn(new Observable()) + when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(new Observable()) // Act page.uploadFile(file); @@ -52,12 +51,10 @@ describe('FileUploadComponent', () => { // Arrange const fixture = MockRender(FileUploadComponent); const page = new Page(fixture); - let fileUploadService = TestBed.inject(FileUploadService); - let uploadMock = mock(); - fileUploadService.upload = uploadMock; + let fileUploadService = mockFileUploadService(); let file = new File([''], 'TestFile.txt'); - when(() => uploadMock(file)).thenReturn(of({ + when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(of({ loaded: 50, total: 100, type: HttpEventType.UploadProgress @@ -78,12 +75,10 @@ describe('FileUploadComponent', () => { // Arrange const fixture = MockRender(FileUploadComponent); const page = new Page(fixture); - let fileUploadService = TestBed.inject(FileUploadService); - let uploadMock = mock(); - fileUploadService.upload = uploadMock; + let fileUploadService = mockFileUploadService(); let file = new File([''], 'TestFile.txt'); - when(() => uploadMock(file)).thenReturn(of({ + when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(of({ type: HttpEventType.Response } as HttpResponse)) let component = fixture.point.componentInstance; diff --git a/src/app/file-upload/file-upload.component.ts b/src/app/file-upload/file-upload.component.ts index be38218..e1b34b5 100644 --- a/src/app/file-upload/file-upload.component.ts +++ b/src/app/file-upload/file-upload.component.ts @@ -1,5 +1,5 @@ import {Component, EventEmitter, Output} from '@angular/core'; -import {FileUploadService} from "./file-upload.service"; +import {FileUploadService, toFileOrBlob} from "./file-upload.service"; import {FileUploadProgress} from "./file-upload-element/file-upload-element.component"; import {HttpEventType} from "@angular/common/http"; import {GooglePickerService} from "./google-picker.service"; @@ -37,7 +37,7 @@ export class FileUploadComponent { private upload(file: File) { let fileProgress: FileUploadProgress = {fileName: file.name, loaded: 0, total: file.size}; this.files.push(fileProgress); - this.fileUploadService.upload(file) + this.fileUploadService.upload(toFileOrBlob(file)) .subscribe(e => { if (e.type === HttpEventType.Response) { this.onRefreshRequest.emit(); diff --git a/src/app/file-upload/file-upload.service.spec.ts b/src/app/file-upload/file-upload.service.spec.ts index 60010fe..edcbafe 100644 --- a/src/app/file-upload/file-upload.service.spec.ts +++ b/src/app/file-upload/file-upload.service.spec.ts @@ -1,10 +1,11 @@ -import {FileUploadService} from './file-upload.service'; -import {MockBuilder, MockRender} from "ng-mocks"; +import {FileUploadService, toFileOrBlob} from './file-upload.service'; +import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; import {HttpClientModule, HttpEventType, HttpProgressEvent, HttpSentEvent} from "@angular/common/http"; import {fakeAsync, TestBed, tick} from "@angular/core/testing"; import {mockBaseFolderService} from "./base-folder.service.spec"; +import {mock} from "strong-mock"; describe('FileUploadService', () => { beforeEach(() => @@ -29,7 +30,7 @@ describe('FileUploadService', () => { // Act let completedRequest = false; - service.upload(f).subscribe(() => completedRequest = true); + service.upload(toFileOrBlob(f)).subscribe(() => completedRequest = true); // Assert tick(); @@ -70,7 +71,7 @@ describe('FileUploadService', () => { // Act let result: any = undefined; - service.upload(f) + service.upload(toFileOrBlob(f)) .subscribe(e => { // Get only the first result if (!result) { @@ -107,3 +108,13 @@ describe('FileUploadService', () => { })) }); + +export function mockFileUploadService() { + let fileUploadServiceMock = mock(); + MockInstance(FileUploadService, () => { + return { + upload: fileUploadServiceMock.upload + } + }); + return fileUploadServiceMock; +} diff --git a/src/app/file-upload/file-upload.service.ts b/src/app/file-upload/file-upload.service.ts index 0f8df8f..cd047f3 100644 --- a/src/app/file-upload/file-upload.service.ts +++ b/src/app/file-upload/file-upload.service.ts @@ -1,5 +1,4 @@ import {Injectable} from '@angular/core'; -import {GoogleDriveAuthService} from "./google-drive-auth.service"; import { HttpClient, HttpEvent, @@ -12,6 +11,18 @@ import { import {catchError, filter, mergeMap, Observable, of} from "rxjs"; import {BaseFolderService} from "./base-folder.service"; +export interface FileOrBlob { + name: string; + blob: Blob; +} + +export function toFileOrBlob(file: File) { + return { + name: file.name, + blob: file + } +} + @Injectable({ providedIn: 'root' }) @@ -19,11 +30,11 @@ export class FileUploadService { private readonly DRIVE_API_UPLOAD_FILES_BASE_URL = 'https://www.googleapis.com/upload/drive/v3/files'; - constructor(private authService: GoogleDriveAuthService, private http: HttpClient, private baseFolderService: BaseFolderService) { + constructor(private http: HttpClient, private baseFolderService: BaseFolderService) { } - upload(file: File): Observable> { - const contentType = file.type || 'application/octet-stream'; + upload(file: FileOrBlob): Observable> { + const contentType = file.blob.type || 'application/octet-stream'; return this.baseFolderService.findOrCreateBaseFolder() .pipe(mergeMap(baseFolderId => { @@ -40,22 +51,22 @@ export class FileUploadService { })) } - private uploadFileToUrl(url: string, contentType: string, file: File) { + private uploadFileToUrl(url: string, contentType: string, file: FileOrBlob) { const uploadHeaders = { 'Content-Type': contentType, 'X-Upload-Content-Type': contentType }; - return this.http.request(new HttpRequest('PUT', url, file, { + return this.http.request(new HttpRequest('PUT', url, file.blob, { headers: new HttpHeaders(uploadHeaders), reportProgress: true })) } - private createUploadFileRequest(file: File, contentType: string, baseFolderId: string) { + private createUploadFileRequest(file: FileOrBlob, contentType: string, baseFolderId: string) { const metadataHeaders = { - 'X-Upload-Content-Length': file.size, + 'X-Upload-Content-Length': file.blob.size, 'X-Upload-Content-Type': contentType }; const url = this.DRIVE_API_UPLOAD_FILES_BASE_URL + '?uploadType=resumable'; @@ -65,7 +76,7 @@ export class FileUploadService { 'parents': [baseFolderId], 'mimeType': contentType, 'Content-Type': contentType, - 'Content-Length': file.size, + 'Content-Length': file.blob.size, }; return this.http.post(url, metadata, { diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index 80116d1..e4039e3 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -2,7 +2,8 @@ import {Rule, RuleRepository} from "./rule.repository"; import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {db} from "../database/db"; -import {mock} from "strong-mock"; +import {mock, when} from "strong-mock"; +import {mockDatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service.spec"; describe('RuleRepository', () => { @@ -62,6 +63,9 @@ describe('RuleRepository', () => { describe('create', () => { it('should persist a new rule', async () => { // Arrange + let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); + when(() => databaseBackupAndRestoreService.backup()).thenResolve(); + const ruleRepository = MockRender(RuleRepository).point.componentInstance; let rule: Rule = { name: 'TestRule', @@ -87,6 +91,10 @@ describe('RuleRepository', () => { describe('delete', () => { it('should delete one rule', async () => { // Arrange + let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); + // 2 calls to 'backup' expected, from create, and then from delete + when(() => databaseBackupAndRestoreService.backup()).thenResolve().times(2); + const ruleRepository = MockRender(RuleRepository).point.componentInstance; let rule: Rule = { name: 'TestRule', diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 051f13d..6959ffb 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -1,5 +1,6 @@ import {Injectable} from '@angular/core'; import {db} from "../database/db"; +import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; export interface Rule { id?: number; @@ -13,21 +14,22 @@ export interface Rule { }) export class RuleRepository { - constructor() { + constructor(private databaseBackupAndRestoreService: DatabaseBackupAndRestoreService) { } - create(rule: Rule): Promise { - return db.rules.add(rule).then(); + async create(rule: Rule) { + await db.rules.add(rule); + await this.databaseBackupAndRestoreService.backup(); } findAll(): Promise { return db.rules.toArray(); } - delete(rule: Rule): Promise { + async delete(rule: Rule) { if (rule.id) { - return db.rules.delete(rule.id); + await db.rules.delete(rule.id); + await this.databaseBackupAndRestoreService.backup(); } - return Promise.resolve(); } } From b5ca98cc362e3b48b3111a12884f44e47ff3bae3 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 17:35:14 +0100 Subject: [PATCH 16/66] [us40] Overwrite the existing db backup instead of creating a new db backup --- ...atabase-backup-and-restore.service.spec.ts | 28 ++- .../database-backup-and-restore.service.ts | 15 +- .../file-upload/file-upload.service.spec.ts | 203 +++++++++++------- src/app/file-upload/file-upload.service.ts | 36 ++-- 4 files changed, 182 insertions(+), 100 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 9400b75..04137d1 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -5,6 +5,8 @@ import {mockFileUploadService} from "../file-upload/file-upload.service.spec"; import {It, mock, when} from "strong-mock"; import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {HttpEventType, HttpResponse} from "@angular/common/http"; +import {mockFileService} from "../file-list/file.service.spec"; +import {mockFileElement} from "../file-list/file-list.component.spec"; describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule)); @@ -18,8 +20,11 @@ describe('DatabaseBackupAndRestoreService', () => { }); describe('backup', () => { - it('should backup one rule', async () => { + it('should upload a new backup file when there is no backup yet', async () => { // Arrange + let fileService = mockFileService(); + when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([])); + let fileUploadService = mockFileUploadService(); when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}))) .thenReturn(mustBeConsumedAsyncObservable({ @@ -34,6 +39,27 @@ describe('DatabaseBackupAndRestoreService', () => { // Assert // No failure in mock setup }) + + it('should overwrite the existing backup file when there is already an existing backup', async () => { + // Arrange + let fileService = mockFileService(); + let dbBackupFile = mockFileElement('db.backup'); + when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([dbBackupFile])); + + let fileUploadService = mockFileUploadService(); + when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}), dbBackupFile.id)) + .thenReturn(mustBeConsumedAsyncObservable({ + type: HttpEventType.Response + } as HttpResponse)); + + const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + + // Act + await databaseBackupAndRestoreService.backup(); + + // Assert + // No failure in mock setup + }) }) }); diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index f29f08c..82b4fa1 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -2,19 +2,26 @@ import {Injectable} from '@angular/core'; import {exportDB} from "dexie-export-import"; import {db} from "./db"; import {FileUploadService} from "../file-upload/file-upload.service"; -import {lastValueFrom} from "rxjs"; +import {lastValueFrom, mergeMap} from "rxjs"; +import {FileService} from "../file-list/file.service"; @Injectable({ providedIn: 'root' }) export class DatabaseBackupAndRestoreService { - constructor(private fileUploadService: FileUploadService) { + private static readonly DB_NAME = 'db.backup'; + + constructor(private fileUploadService: FileUploadService, private fileService: FileService) { } async backup() { let blob = await exportDB(db); - // TODO: Don't create the file when it already exists - await lastValueFrom(this.fileUploadService.upload({name: 'db.backup', blob})); + await lastValueFrom(this.fileService.findAll() + .pipe(mergeMap(files => { + // TODO: ensure there cannot be any conflicts with user files + let dbFile = files.find(file => file.name === DatabaseBackupAndRestoreService.DB_NAME); + return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); + }))); } } diff --git a/src/app/file-upload/file-upload.service.spec.ts b/src/app/file-upload/file-upload.service.spec.ts index edcbafe..1e5d5cb 100644 --- a/src/app/file-upload/file-upload.service.spec.ts +++ b/src/app/file-upload/file-upload.service.spec.ts @@ -21,92 +21,129 @@ describe('FileUploadService', () => { expect(service).toBeTruthy(); }); - it('should upload', fakeAsync(() => { - // Arrange - let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); - mockBaseFolderService() - const service = MockRender(FileUploadService).point.componentInstance; - let httpTestingController = TestBed.inject(HttpTestingController); - - // Act - let completedRequest = false; - service.upload(toFileOrBlob(f)).subscribe(() => completedRequest = true); - - // Assert - tick(); - - // The following 2 requests are expected: create upload, upload - const req = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual({ - name: 'test.txt', - parents: ['baseFolderId'], - mimeType: 'application/txt', - 'Content-Type': 'application/txt', - 'Content-Length': 12 - }); - req.flush('', {headers: {'Location': 'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'}}); - - const req2 = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'); - expect(req2.request.method).toEqual('PUT'); - expect(req2.request.body).toEqual(f); - req2.event({total: 100, loaded: 100, type: HttpEventType.UploadProgress}) - req2.flush(''); - - expect(completedRequest).toBeTrue(); - - // Finally, assert that there are no outstanding requests. - httpTestingController.verify(); - })) - - /** - * Send two events when uploading and make sure we filter the unwanted one - */ - it('should filter out unwanted http events when uploading', fakeAsync(() => { - // Arrange - let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); - mockBaseFolderService(); - const service = MockRender(FileUploadService).point.componentInstance; - let httpTestingController = TestBed.inject(HttpTestingController); - - // Act - let result: any = undefined; - service.upload(toFileOrBlob(f)) - .subscribe(e => { - // Get only the first result - if (!result) { - result = e; - } + describe('upload', () => { + it('should upload', fakeAsync(() => { + // Arrange + let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); + mockBaseFolderService() + const service = MockRender(FileUploadService).point.componentInstance; + let httpTestingController = TestBed.inject(HttpTestingController); + + // Act + let completedRequest = false; + service.upload(toFileOrBlob(f)).subscribe(() => completedRequest = true); + + // Assert + tick(); + + // The following 2 requests are expected: create upload, upload + const req = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual({ + name: 'test.txt', + parents: ['baseFolderId'], + mimeType: 'application/txt', + 'Content-Type': 'application/txt', + 'Content-Length': 12 + }); + req.flush('', {headers: {'Location': 'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'}}); + + const req2 = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'); + expect(req2.request.method).toEqual('PUT'); + expect(req2.request.body).toEqual(f); + req2.event({total: 100, loaded: 100, type: HttpEventType.UploadProgress}) + req2.flush(''); + + expect(completedRequest).toBeTrue(); + + // Finally, assert that there are no outstanding requests. + httpTestingController.verify(); + })) + + /** + * Send two events when uploading and make sure we filter the unwanted one + */ + it('should filter out unwanted http events when uploading', fakeAsync(() => { + // Arrange + let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); + mockBaseFolderService(); + const service = MockRender(FileUploadService).point.componentInstance; + let httpTestingController = TestBed.inject(HttpTestingController); + + // Act + let result: any = undefined; + service.upload(toFileOrBlob(f)) + .subscribe(e => { + // Get only the first result + if (!result) { + result = e; + } + }); + + // Assert + tick(); + // The following 2 requests are expected: create upload, upload + const req = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual({ + name: 'test.txt', + parents: ['baseFolderId'], + mimeType: 'application/txt', + 'Content-Type': 'application/txt', + 'Content-Length': 12 + }); + req.flush('', {headers: {'Location': 'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'}}); + + const req2 = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'); + expect(req2.request.method).toEqual('PUT'); + expect(req2.request.body).toEqual(f); + let event: HttpSentEvent = {type: HttpEventType.Sent}; + req2.event(event) + let event2: HttpProgressEvent = {type: HttpEventType.UploadProgress, total: 100, loaded: 50} + req2.event(event2) + + expect(result.type).toEqual(HttpEventType.UploadProgress); + + // Finally, assert that there are no outstanding requests. + httpTestingController.verify(); + })) + + it('should overwrite an existing file when provided with an id', fakeAsync(() => { + // Arrange + let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); + mockBaseFolderService() + const service = MockRender(FileUploadService).point.componentInstance; + let httpTestingController = TestBed.inject(HttpTestingController); + + // Act + let completedRequest = false; + service.upload(toFileOrBlob(f), 'fileId4894') + .subscribe(() => completedRequest = true); + + // Assert + tick(); + + // The following 2 requests are expected: create upload, upload + const req = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files/fileId4894?uploadType=resumable'); + expect(req.request.method).toEqual('PATCH'); + expect(req.request.body).toEqual({ + 'Content-Type': 'application/txt', + 'Content-Length': 12 }); + req.flush('', {headers: {'Location': 'https://www.googleapis.com/upload/drive/v3/files/fileId4894?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'}}); - // Assert - tick(); - // The following 2 requests are expected: create upload, upload - const req = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable'); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual({ - name: 'test.txt', - parents: ['baseFolderId'], - mimeType: 'application/txt', - 'Content-Type': 'application/txt', - 'Content-Length': 12 - }); - req.flush('', {headers: {'Location': 'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'}}); - - const req2 = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'); - expect(req2.request.method).toEqual('PUT'); - expect(req2.request.body).toEqual(f); - let event: HttpSentEvent = {type: HttpEventType.Sent}; - req2.event(event) - let event2: HttpProgressEvent = {type: HttpEventType.UploadProgress, total: 100, loaded: 50} - req2.event(event2) - - expect(result.type).toEqual(HttpEventType.UploadProgress); - - // Finally, assert that there are no outstanding requests. - httpTestingController.verify(); - })) + const req2 = httpTestingController.expectOne('https://www.googleapis.com/upload/drive/v3/files/fileId4894?uploadType=resumable&upload_id=ADPycdtRB5_hUde03FI0b'); + expect(req2.request.method).toEqual('PUT'); + expect(req2.request.body).toEqual(f); + req2.event({total: 100, loaded: 100, type: HttpEventType.UploadProgress}) + req2.flush(''); + + expect(completedRequest).toBeTrue(); + // Finally, assert that there are no outstanding requests. + httpTestingController.verify(); + })) + }) }); export function mockFileUploadService() { diff --git a/src/app/file-upload/file-upload.service.ts b/src/app/file-upload/file-upload.service.ts index cd047f3..25e7c0f 100644 --- a/src/app/file-upload/file-upload.service.ts +++ b/src/app/file-upload/file-upload.service.ts @@ -33,12 +33,12 @@ export class FileUploadService { constructor(private http: HttpClient, private baseFolderService: BaseFolderService) { } - upload(file: FileOrBlob): Observable> { + upload(file: FileOrBlob, fileId?: string): Observable> { const contentType = file.blob.type || 'application/octet-stream'; return this.baseFolderService.findOrCreateBaseFolder() .pipe(mergeMap(baseFolderId => { - return this.createUploadFileRequest(file, contentType, baseFolderId); + return this.createUploadFileRequest(file, contentType, baseFolderId, fileId); }), mergeMap(metadataRes => { const locationUrl = metadataRes.headers.get('Location') ?? ''; @@ -63,25 +63,37 @@ export class FileUploadService { })) } - private createUploadFileRequest(file: FileOrBlob, contentType: string, baseFolderId: string) { + private createUploadFileRequest(file: FileOrBlob, contentType: string, baseFolderId: string, fileId?: string) { const metadataHeaders = { 'X-Upload-Content-Length': file.blob.size, 'X-Upload-Content-Type': contentType }; - const url = this.DRIVE_API_UPLOAD_FILES_BASE_URL + '?uploadType=resumable'; + let url = this.DRIVE_API_UPLOAD_FILES_BASE_URL; + if (fileId) { + url += '/' + fileId; + } + url += '?uploadType=resumable'; - const metadata = { - 'name': file.name, - 'parents': [baseFolderId], - 'mimeType': contentType, + let metadata: any = { 'Content-Type': contentType, 'Content-Length': file.blob.size, }; - return this.http.post(url, metadata, { - headers: new HttpHeaders(metadataHeaders), - observe: 'response' - }); + if (fileId) { + return this.http.patch(url, metadata, { + headers: new HttpHeaders(metadataHeaders), + observe: 'response' + }); + } else { + metadata.name = file.name; + metadata.parents = [baseFolderId]; + metadata.mimeType = contentType; + + return this.http.post(url, metadata, { + headers: new HttpHeaders(metadataHeaders), + observe: 'response' + }); + } } } From 818a34af0297f09bb52a252f509b0b3a1d759e3b Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 17:38:41 +0100 Subject: [PATCH 17/66] [us40] Fix failing test in FileUploadComponent because of mocking after MockRender --- .../file-upload/file-upload.component.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/file-upload/file-upload.component.spec.ts b/src/app/file-upload/file-upload.component.spec.ts index 40a02ee..6ee3d2a 100644 --- a/src/app/file-upload/file-upload.component.spec.ts +++ b/src/app/file-upload/file-upload.component.spec.ts @@ -32,13 +32,13 @@ describe('FileUploadComponent', () => { describe('When selecting a file to upload', () => { it('Should shows the file as being uploaded', () => { // Arrange - const fixture = MockRender(FileUploadComponent); - const page = new Page(fixture); let fileUploadService = mockFileUploadService(); - let file = new File([''], 'TestFile.txt'); when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(new Observable()) + const fixture = MockRender(FileUploadComponent); + const page = new Page(fixture); + // Act page.uploadFile(file); @@ -49,10 +49,7 @@ describe('FileUploadComponent', () => { it('Should update upload progress', () => { // Arrange - const fixture = MockRender(FileUploadComponent); - const page = new Page(fixture); let fileUploadService = mockFileUploadService(); - let file = new File([''], 'TestFile.txt'); when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(of({ loaded: 50, @@ -60,6 +57,9 @@ describe('FileUploadComponent', () => { type: HttpEventType.UploadProgress })) + const fixture = MockRender(FileUploadComponent); + const page = new Page(fixture); + // Act page.uploadFile(file); @@ -73,14 +73,14 @@ describe('FileUploadComponent', () => { it('Should trigger upload finish event', () => { // Arrange - const fixture = MockRender(FileUploadComponent); - const page = new Page(fixture); let fileUploadService = mockFileUploadService(); - let file = new File([''], 'TestFile.txt'); when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(of({ type: HttpEventType.Response } as HttpResponse)) + + const fixture = MockRender(FileUploadComponent); + const page = new Page(fixture); let component = fixture.point.componentInstance; let finishedEventReceived = false; component.onRefreshRequest.subscribe(() => finishedEventReceived = true) From cce3abbc5ba6a8867685b3600c9dc13739913af1 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 30 Nov 2023 17:40:29 +0100 Subject: [PATCH 18/66] [us40] Fix missing import of dexie --- package-lock.json | 22 ++++++++++++++++++++-- package.json | 4 +++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ba9975..61f4d33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "storemydocs", - "version": "0.2.0-26-g2444cfa", + "version": "0.3.1-12-g5d0e863", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "storemydocs", - "version": "0.2.0-26-g2444cfa", + "version": "0.3.1-12-g5d0e863", "dependencies": { "@angular/animations": "^16.1.0", "@angular/cdk": "^16.1.4", @@ -19,6 +19,8 @@ "@angular/platform-browser-dynamic": "^16.2.12", "@angular/router": "^16.2.12", "@auth0/angular-jwt": "^5.2.0", + "dexie": "^3.2.4", + "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", "ngx-filesize": "^3.0.2", "rxjs": "~7.8.0", @@ -7038,6 +7040,22 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/dexie": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz", + "integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/dexie-export-import": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/dexie-export-import/-/dexie-export-import-4.0.7.tgz", + "integrity": "sha512-h22soiockhhWch6edw8XL/JNfn7akPLuLf6kPQdR4uneG/P0XQus4I8wpjV86dck61oEYKPHm36jyft/zVK0jQ==", + "peerDependencies": { + "dexie": "^2.0.4 || ^3.0.0 || ^4.0.1-alpha.5" + } + }, "node_modules/di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", diff --git a/package.json b/package.json index 0180942..100a8ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "storemydocs", - "version": "0.2.0-26-g2444cfa", + "version": "0.3.1-12-g5d0e863", "scripts": { "ng": "ng", "start": "ng serve", @@ -28,6 +28,8 @@ "@angular/platform-browser-dynamic": "^16.2.12", "@angular/router": "^16.2.12", "@auth0/angular-jwt": "^5.2.0", + "dexie": "^3.2.4", + "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", "ngx-filesize": "^3.0.2", "rxjs": "~7.8.0", From 86479099d6b7c215fe875c41220fad81ae7888cb Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 1 Dec 2023 15:23:45 +0100 Subject: [PATCH 19/66] [us40] Implement db restore functionality --- ...atabase-backup-and-restore.service.spec.ts | 81 +++++++++++++++++-- .../database-backup-and-restore.service.ts | 44 +++++++--- src/app/rules/rule.repository.spec.ts | 19 +++-- src/app/rules/rule.repository.ts | 4 +- src/testing/common-testing-function.spec.ts | 7 ++ 5 files changed, 130 insertions(+), 25 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 04137d1..b7217c6 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -3,13 +3,24 @@ import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {mockFileUploadService} from "../file-upload/file-upload.service.spec"; import {It, mock, when} from "strong-mock"; -import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; -import {HttpEventType, HttpResponse} from "@angular/common/http"; +import {dbCleanUp, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; +import {HttpClientModule, HttpEventType, HttpResponse} from "@angular/common/http"; import {mockFileService} from "../file-list/file.service.spec"; import {mockFileElement} from "../file-list/file-list.component.spec"; +import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; +import {fakeAsync, TestBed, tick} from "@angular/core/testing"; +import {db} from "./db"; +import {lastValueFrom} from "rxjs"; describe('DatabaseBackupAndRestoreService', () => { - beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule)); + beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) + .replace(HttpClientModule, HttpClientTestingModule) + ); + + // Db cleanup after each test + afterEach(async () => { + await dbCleanUp(); + }); it('should be created', () => { // Act @@ -19,6 +30,62 @@ describe('DatabaseBackupAndRestoreService', () => { expect(databaseBackupAndRestoreService).toBeTruthy(); }); + describe('restore', () => { + it('the database should be automatically restored', fakeAsync(async () => { + // Arrange + let fileService = mockFileService(); + let dbBackupFile = mockFileElement('db.backup'); + when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([dbBackupFile])); + + let fixture = MockRender(DatabaseBackupAndRestoreService); + let databaseBackupAndRestoreService = fixture.point.componentInstance; + + // Act + let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore()); + + // Assert + tick(); + let httpTestingController = TestBed.inject(HttpTestingController); + + let request = httpTestingController.expectOne('https://www.googleapis.com/drive/v3/files/' + dbBackupFile.id + '?alt=media'); + request.flush(new Blob([JSON.stringify({ + "formatName": "dexie", + "formatVersion": 1, + "data": { + "databaseName": "StoreMyDocsDB", + "databaseVersion": 3, + "tables": [{"name": "rules", "schema": "++id", "rowCount": 1}], + "data": [{ + "tableName": "rules", + "inbound": true, + "rows": [{ + "name": "TestRule", + "category": ["Test1", "ChildTest1"], + "script": "return true", + "id": 1, + "$types": {"category": "arrayNonindexKeys"} + }] + }] + } + })])); + + // We need to explicitly wait for the restore to finish + await restorePromise; + + let rules = await db.rules.toArray(); + expect(rules) + .toEqual([{ + id: 1, + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }]); + + httpTestingController.verify(); + })); + }) + + describe('backup', () => { it('should upload a new backup file when there is no backup yet', async () => { // Arrange @@ -34,10 +101,11 @@ describe('DatabaseBackupAndRestoreService', () => { const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; // Act - await databaseBackupAndRestoreService.backup(); + let backupPromise = lastValueFrom(databaseBackupAndRestoreService.backup()); // Assert // No failure in mock setup + await backupPromise; }) it('should overwrite the existing backup file when there is already an existing backup', async () => { @@ -55,11 +123,12 @@ describe('DatabaseBackupAndRestoreService', () => { const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; // Act - await databaseBackupAndRestoreService.backup(); + let backupPromise = lastValueFrom(databaseBackupAndRestoreService.backup()); // Assert // No failure in mock setup - }) + await backupPromise; + }); }) }); diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 82b4fa1..c755cec 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -1,9 +1,11 @@ import {Injectable} from '@angular/core'; -import {exportDB} from "dexie-export-import"; +import {exportDB, importDB} from "dexie-export-import"; import {db} from "./db"; import {FileUploadService} from "../file-upload/file-upload.service"; -import {lastValueFrom, mergeMap} from "rxjs"; +import {from, map, mergeMap, Observable, of} from "rxjs"; import {FileService} from "../file-list/file.service"; +import {FileElement, isFileElement} from "../file-list/file-list.component"; +import {HttpClient} from "@angular/common/http"; @Injectable({ providedIn: 'root' @@ -12,16 +14,38 @@ export class DatabaseBackupAndRestoreService { private static readonly DB_NAME = 'db.backup'; - constructor(private fileUploadService: FileUploadService, private fileService: FileService) { + constructor(private fileUploadService: FileUploadService, private fileService: FileService, private http: HttpClient) { } - async backup() { - let blob = await exportDB(db); - await lastValueFrom(this.fileService.findAll() - .pipe(mergeMap(files => { + backup() { + return from(exportDB(db)) + .pipe(mergeMap(blob => { + return this.findExistingDbFile() + .pipe(mergeMap(dbFile => { + return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); + })); + })); + } + + restore(): Observable { + return this.findExistingDbFile().pipe(mergeMap(dbFile => { + if (dbFile) { + let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + dbFile.id + '?alt=media'; + return this.http.get(dlLink, {responseType: "blob"}); + } + return of(); + }), mergeMap(dbDownloadResponse => { + return from(importDB(dbDownloadResponse)); + }), map(() => void 0)); + } + + private findExistingDbFile() { + return this.fileService.findAll() + .pipe(map(files => { // TODO: ensure there cannot be any conflicts with user files - let dbFile = files.find(file => file.name === DatabaseBackupAndRestoreService.DB_NAME); - return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); - }))); + return files.filter(f => isFileElement(f)) + .map(f => f as FileElement) + .find(file => file.name === DatabaseBackupAndRestoreService.DB_NAME) + })); } } diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index e4039e3..79ddf1f 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -4,16 +4,22 @@ import {AppModule} from "../app.module"; import {db} from "../database/db"; import {mock, when} from "strong-mock"; import {mockDatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service.spec"; +import {dbCleanUp, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; +import {HttpEventType, HttpResponse} from "@angular/common/http"; +function mockBackupCall() { + let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); + return when(() => databaseBackupAndRestoreService.backup()) + .thenReturn(mustBeConsumedAsyncObservable({type: HttpEventType.Response} as HttpResponse)); +} + describe('RuleRepository', () => { beforeEach(() => MockBuilder(RuleRepository, AppModule)); // Db cleanup after each test afterEach(async () => { - await db.delete(); - db.createSchema(); - await db.open(); + await dbCleanUp(); }); it('should be created', () => { @@ -27,6 +33,7 @@ describe('RuleRepository', () => { describe('findAll', () => { it('should list two rules', async () => { // Arrange + mockBackupCall().times(2); const ruleRepository = MockRender(RuleRepository).point.componentInstance; let rule1: Rule = { name: 'TestRule', @@ -63,8 +70,7 @@ describe('RuleRepository', () => { describe('create', () => { it('should persist a new rule', async () => { // Arrange - let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); - when(() => databaseBackupAndRestoreService.backup()).thenResolve(); + mockBackupCall(); const ruleRepository = MockRender(RuleRepository).point.componentInstance; let rule: Rule = { @@ -91,9 +97,8 @@ describe('RuleRepository', () => { describe('delete', () => { it('should delete one rule', async () => { // Arrange - let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); // 2 calls to 'backup' expected, from create, and then from delete - when(() => databaseBackupAndRestoreService.backup()).thenResolve().times(2); + mockBackupCall().times(2); const ruleRepository = MockRender(RuleRepository).point.componentInstance; let rule: Rule = { diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 6959ffb..24ed1a0 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -19,7 +19,7 @@ export class RuleRepository { async create(rule: Rule) { await db.rules.add(rule); - await this.databaseBackupAndRestoreService.backup(); + this.databaseBackupAndRestoreService.backup().subscribe(); } findAll(): Promise { @@ -29,7 +29,7 @@ export class RuleRepository { async delete(rule: Rule) { if (rule.id) { await db.rules.delete(rule.id); - await this.databaseBackupAndRestoreService.backup(); + this.databaseBackupAndRestoreService.backup().subscribe(); } } } diff --git a/src/testing/common-testing-function.spec.ts b/src/testing/common-testing-function.spec.ts index a164f02..8284743 100644 --- a/src/testing/common-testing-function.spec.ts +++ b/src/testing/common-testing-function.spec.ts @@ -1,4 +1,5 @@ import {Observable, Subscriber, TeardownLogic} from "rxjs"; +import {db} from "../app/database/db"; export async function findAsyncSequential( array: T[], @@ -61,3 +62,9 @@ class TestObservable extends Observable { return this.id; } } + +export async function dbCleanUp() { + await db.delete(); + db.createSchema(); + await db.open(); +} From fee051c6f93a28e5e9e525006dccd999b69ab315 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 1 Dec 2023 17:28:14 +0100 Subject: [PATCH 20/66] [us40] Show database backup progress in a snackbar --- src/app/app.module.ts | 4 +- .../background-task.service.spec.ts | 85 +++++++++++++++++++ .../background-task.service.ts | 46 ++++++++++ .../progress-indicator.snack-bar.html | 3 + .../progress-indicator.snack-bar.scss | 3 + ...atabase-backup-and-restore.service.spec.ts | 4 + .../database-backup-and-restore.service.ts | 9 +- 7 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 src/app/background-task/background-task.service.spec.ts create mode 100644 src/app/background-task/background-task.service.ts create mode 100644 src/app/background-task/progress-indicator.snack-bar.html create mode 100644 src/app/background-task/progress-indicator.snack-bar.scss diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d9429a5..444204c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -33,6 +33,7 @@ import {TitleHeaderComponent} from './title-header/title-header.component'; import {MatSortModule} from "@angular/material/sort"; import {RulesComponent} from './rules/rules.component'; import {MatExpansionModule} from "@angular/material/expansion"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; @NgModule({ declarations: [ @@ -70,7 +71,8 @@ import {MatExpansionModule} from "@angular/material/expansion"; MatTreeModule, MatChipsModule, MatSortModule, - MatExpansionModule + MatExpansionModule, + MatSnackBarModule ], providers: [ httpInterceptorProviders diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts new file mode 100644 index 0000000..5598626 --- /dev/null +++ b/src/app/background-task/background-task.service.spec.ts @@ -0,0 +1,85 @@ +import {BackgroundTaskService} from './background-task.service'; +import {mock} from "strong-mock"; +import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; +import {AppModule} from "../app.module"; +import {of} from "rxjs"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; +import {HttpEventType} from "@angular/common/http"; +import {ComponentFixture} from "@angular/core/testing"; +import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; +import {HarnessLoader} from "@angular/cdk/testing"; +import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; +import {MatSnackBarHarness} from "@angular/material/snack-bar/testing"; + +describe('BackgroundTaskService', () => { + beforeEach(() => MockBuilder(BackgroundTaskService, AppModule) + .keep(MatSnackBarModule) + .replace(BrowserAnimationsModule, NoopAnimationsModule) + ); + + it('should be created', () => { + // Act + const backgroundTaskService = MockRender(BackgroundTaskService).point.componentInstance; + + // Assert + expect(backgroundTaskService).toBeTruthy(); + }); + + describe('showProgress', () => { + it('Should show initial 0 progress', async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + let page = new Page(fixture); + const backgroundTaskService = fixture.point.componentInstance; + + // Act + backgroundTaskService.showProgress(of()); + + // Assert + let result = await page.getProgressMessage(); + expect(result).toEqual("0%: Database backup in progress..."); + }) + + it('Should show in progress', async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + let page = new Page(fixture); + const backgroundTaskService = fixture.point.componentInstance; + + // Act + backgroundTaskService.showProgress(of({ + type: HttpEventType.UploadProgress, + loaded: 50, + total: 200 + })); + + // Assert + let result = await page.getProgressMessage(); + expect(result).toEqual("25%: Database backup in progress..."); + }) + }) +}); + +class Page { + private loader: HarnessLoader; + + constructor(fixture: ComponentFixture) { + this.loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + } + + async getProgressMessage() { + + let snackBar = await this.loader.getHarness(MatSnackBarHarness); + return snackBar.getMessage(); + } +} + +export function mockBackgroundTaskService() { + let backgroundTaskService = mock(); + MockInstance(BackgroundTaskService, () => { + return { + showProgress: backgroundTaskService.showProgress + } + }); + return backgroundTaskService; +} diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts new file mode 100644 index 0000000..67764f1 --- /dev/null +++ b/src/app/background-task/background-task.service.ts @@ -0,0 +1,46 @@ +import {Component, Inject, Injectable} from '@angular/core'; +import {Observable} from "rxjs"; +import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; +import {MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarModule} from "@angular/material/snack-bar"; + +@Injectable({ + providedIn: 'root' +}) +export class BackgroundTaskService { + + constructor(private snackBar: MatSnackBar) { + } + + showProgress(observable: Observable>): void { + let progressData = {progress: 0}; + this.openSnackBar(progressData); + + observable.subscribe(value => { + if (value.type === HttpEventType.UploadProgress && value.total) { + progressData.progress = (100 * value.loaded) / value.total + } + }) + } + + private openSnackBar(data: ProgressData) { + return this.snackBar.openFromComponent(SnackBarProgressIndicatorComponent, {data: data}); + } +} + +interface ProgressData { + progress: number +} + +@Component({ + selector: 'app-progress-indicator-snack-bar', + templateUrl: 'progress-indicator.snack-bar.html', + styleUrls: ['./progress-indicator.snack-bar.scss'], + standalone: true, + imports: [ + MatSnackBarModule + ] +}) +class SnackBarProgressIndicatorComponent { + constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ProgressData) { + } +} diff --git a/src/app/background-task/progress-indicator.snack-bar.html b/src/app/background-task/progress-indicator.snack-bar.html new file mode 100644 index 0000000..192e013 --- /dev/null +++ b/src/app/background-task/progress-indicator.snack-bar.html @@ -0,0 +1,3 @@ + +{{data.progress}}%: Database backup in progress... + diff --git a/src/app/background-task/progress-indicator.snack-bar.scss b/src/app/background-task/progress-indicator.snack-bar.scss new file mode 100644 index 0000000..79ff2d5 --- /dev/null +++ b/src/app/background-task/progress-indicator.snack-bar.scss @@ -0,0 +1,3 @@ +:host { + display: flex; +} diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index b7217c6..eb9763c 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -11,6 +11,7 @@ import {HttpClientTestingModule, HttpTestingController} from "@angular/common/ht import {fakeAsync, TestBed, tick} from "@angular/core/testing"; import {db} from "./db"; import {lastValueFrom} from "rxjs"; +import {mockBackgroundTaskService} from "../background-task/background-task.service.spec"; describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) @@ -98,6 +99,9 @@ describe('DatabaseBackupAndRestoreService', () => { type: HttpEventType.Response } as HttpResponse)); + let backgroundTaskService = mockBackgroundTaskService(); + when(() => backgroundTaskService.showProgress(It.isAny())).thenReturn(); + const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; // Act diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index c755cec..db91600 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -6,6 +6,7 @@ import {from, map, mergeMap, Observable, of} from "rxjs"; import {FileService} from "../file-list/file.service"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {HttpClient} from "@angular/common/http"; +import {BackgroundTaskService} from "../background-task/background-task.service"; @Injectable({ providedIn: 'root' @@ -14,17 +15,21 @@ export class DatabaseBackupAndRestoreService { private static readonly DB_NAME = 'db.backup'; - constructor(private fileUploadService: FileUploadService, private fileService: FileService, private http: HttpClient) { + constructor(private fileUploadService: FileUploadService, private fileService: FileService, private http: HttpClient, + private backgroundTaskService: BackgroundTaskService) { } backup() { - return from(exportDB(db)) + let observable = from(exportDB(db)) .pipe(mergeMap(blob => { return this.findExistingDbFile() .pipe(mergeMap(dbFile => { return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); })); })); + // TODO: move this to a pipe function? + this.backgroundTaskService.showProgress(observable); + return observable; } restore(): Observable { From 0bfd1db5109a72ea918d282dc7c56bb134f5e452 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 1 Dec 2023 17:47:00 +0100 Subject: [PATCH 21/66] [us40] Fix tests failing due to mocking BreakpointObserver (a dependency of the snackbar module) --- src/app/file-list/file-list.component.spec.ts | 2 + .../file-upload-element.component.spec.ts | 78 ++++++++++--------- .../file-upload/file-upload.component.spec.ts | 4 +- src/app/rules/rules.component.spec.ts | 2 + 4 files changed, 47 insertions(+), 39 deletions(-) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index b6436d8..90acca2 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -30,6 +30,7 @@ import {MatChipGridHarness} from "@angular/material/chips/testing"; import {mockFileService} from "./file.service.spec"; import {MatSortModule} from "@angular/material/sort"; import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; +import {BreakpointObserver} from "@angular/cdk/layout"; function mockRenderAndWaitForChanges() { let fixture = MockRender(FileListComponent); @@ -49,6 +50,7 @@ describe('FileListComponent', () => { .keep(MatTreeModule) .keep(MatChipsModule) .keep(MatSortModule) + .keep(BreakpointObserver) .replace(BrowserAnimationsModule, NoopAnimationsModule) ); diff --git a/src/app/file-upload/file-upload-element/file-upload-element.component.spec.ts b/src/app/file-upload/file-upload-element/file-upload-element.component.spec.ts index 434217c..ae02964 100644 --- a/src/app/file-upload/file-upload-element/file-upload-element.component.spec.ts +++ b/src/app/file-upload/file-upload-element/file-upload-element.component.spec.ts @@ -6,52 +6,54 @@ import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; import {HarnessLoader} from "@angular/cdk/testing"; import {MatProgressBarHarness} from "@angular/material/progress-bar/testing"; import {MatProgressBarModule} from "@angular/material/progress-bar"; +import {BreakpointObserver} from "@angular/cdk/layout"; describe('FileUploadElementComponent', () => { - beforeEach(() => { - return MockBuilder(FileUploadElementComponent, AppModule) - .keep(MatProgressBarModule); - }); - - it('should create', () => { - // Arrange - const fixture = MockRender(FileUploadElementComponent); - const component = fixture.point.componentInstance; - - // Assert - expect(component).toBeTruthy(); - }); - describe('When a file is partially uploaded', () => { - it('should show current progress', async () => { - // Arrange - let fileProgress: FileUploadProgress = {fileName: 'Test.txt', total: 100, loaded: 40}; - - // Act - const fixture = MockRender(FileUploadElementComponent, {fileProgress: fileProgress} as FileUploadElementComponent); - - // Assert - const page = new Page(fixture); - expect(page.getFileName()).toEqual('Test.txt'); - expect(await page.getUploadProgress()).toEqual(40); - - }) + beforeEach(() => { + return MockBuilder(FileUploadElementComponent, AppModule) + .keep(BreakpointObserver) + .keep(MatProgressBarModule); + }); + + it('should create', () => { + // Arrange + const fixture = MockRender(FileUploadElementComponent); + const component = fixture.point.componentInstance; + + // Assert + expect(component).toBeTruthy(); + }); + describe('When a file is partially uploaded', () => { + it('should show current progress', async () => { + // Arrange + let fileProgress: FileUploadProgress = {fileName: 'Test.txt', total: 100, loaded: 40}; + + // Act + const fixture = MockRender(FileUploadElementComponent, {fileProgress: fileProgress} as FileUploadElementComponent); + + // Assert + const page = new Page(fixture); + expect(page.getFileName()).toEqual('Test.txt'); + expect(await page.getUploadProgress()).toEqual(40); + }) + }) }); class Page { - private loader: HarnessLoader; + private loader: HarnessLoader; - constructor(fixture: ComponentFixture) { - this.loader = TestbedHarnessEnvironment.loader(fixture); - } + constructor(fixture: ComponentFixture) { + this.loader = TestbedHarnessEnvironment.loader(fixture); + } - getFileName(): string { - return ngMocks.find('span').nativeNode.textContent.trim(); - } + getFileName(): string { + return ngMocks.find('span').nativeNode.textContent.trim(); + } - async getUploadProgress() { - let progressBar = await this.loader.getHarness(MatProgressBarHarness); - return progressBar.getValue(); - } + async getUploadProgress() { + let progressBar = await this.loader.getHarness(MatProgressBarHarness); + return progressBar.getValue(); + } } diff --git a/src/app/file-upload/file-upload.component.spec.ts b/src/app/file-upload/file-upload.component.spec.ts index 6ee3d2a..023b6c9 100644 --- a/src/app/file-upload/file-upload.component.spec.ts +++ b/src/app/file-upload/file-upload.component.spec.ts @@ -13,11 +13,13 @@ import {HarnessLoader} from "@angular/cdk/testing"; import {MatButtonHarness} from "@angular/material/button/testing"; import {GooglePickerService} from "./google-picker.service"; import {mockFileUploadService} from "./file-upload.service.spec"; +import {BreakpointObserver} from "@angular/cdk/layout"; describe('FileUploadComponent', () => { beforeEach(() => { return MockBuilder(FileUploadComponent, AppModule) - .keep(MatIconModule); + .keep(MatIconModule) + .keep(BreakpointObserver) }); it('should create', () => { diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index aefe44e..9dfef03 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -17,6 +17,7 @@ import {FormsModule} from "@angular/forms"; import {MatChipsModule} from "@angular/material/chips"; import {MatChipGridHarness} from "@angular/material/chips/testing"; import {Rule} from "./rule.repository"; +import {BreakpointObserver} from "@angular/cdk/layout"; describe('RulesComponent', () => { beforeEach(() => MockBuilder(RulesComponent, AppModule) @@ -24,6 +25,7 @@ describe('RulesComponent', () => { .keep(MatFormFieldModule) .keep(FormsModule) .keep(MatChipsModule) + .keep(BreakpointObserver) .replace(BrowserAnimationsModule, NoopAnimationsModule) ); From 1f989da65d158feccdb5e9c41485fadfbf1e35ae Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 7 Dec 2023 14:48:56 +0100 Subject: [PATCH 22/66] [us40] Show finished message in snackbar and dismiss message automatically --- .../background-task.service.spec.ts | 37 ++++++++++++++++--- .../background-task.service.ts | 21 +++++++---- .../progress-indicator.snack-bar.html | 4 +- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 5598626..9b1711c 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -5,11 +5,10 @@ import {AppModule} from "../app.module"; import {of} from "rxjs"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {HttpEventType} from "@angular/common/http"; -import {ComponentFixture} from "@angular/core/testing"; +import {ComponentFixture, fakeAsync, tick} from "@angular/core/testing"; import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; import {HarnessLoader} from "@angular/cdk/testing"; import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; -import {MatSnackBarHarness} from "@angular/material/snack-bar/testing"; describe('BackgroundTaskService', () => { beforeEach(() => MockBuilder(BackgroundTaskService, AppModule) @@ -54,9 +53,34 @@ describe('BackgroundTaskService', () => { })); // Assert + fixture.detectChanges(); let result = await page.getProgressMessage(); expect(result).toEqual("25%: Database backup in progress..."); }) + + it('Should show as completed and should dismiss after 3s', fakeAsync(async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + let page = new Page(fixture); + const backgroundTaskService = fixture.point.componentInstance; + + // Act + backgroundTaskService.showProgress(of({ + type: HttpEventType.UploadProgress, + loaded: 200, + total: 200 + })); + + // Assert + fixture.detectChanges(); + let resultMessage = await page.getProgressMessage(); + expect(resultMessage).toEqual("100%: Database backup finished!"); + tick(3000); + // The message should be gone after 5 seconds at least + resultMessage = await page.getProgressMessage(); + expect(resultMessage).toEqual(undefined); + + })) }) }); @@ -68,9 +92,12 @@ class Page { } async getProgressMessage() { - - let snackBar = await this.loader.getHarness(MatSnackBarHarness); - return snackBar.getMessage(); + let element = document.body.querySelector('mat-snack-bar-container'); + if (element) { + let textContent = element.textContent; + return textContent || undefined; + } + return undefined } } diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index 67764f1..d811ae3 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -1,7 +1,8 @@ import {Component, Inject, Injectable} from '@angular/core'; -import {Observable} from "rxjs"; +import {BehaviorSubject, Observable} from "rxjs"; import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; -import {MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarModule} from "@angular/material/snack-bar"; +import {MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarModule, MatSnackBarRef} from "@angular/material/snack-bar"; +import {NgIf} from "@angular/common"; @Injectable({ providedIn: 'root' @@ -12,12 +13,12 @@ export class BackgroundTaskService { } showProgress(observable: Observable>): void { - let progressData = {progress: 0}; + let progressData: ProgressData = {progress: new BehaviorSubject(0)}; this.openSnackBar(progressData); observable.subscribe(value => { if (value.type === HttpEventType.UploadProgress && value.total) { - progressData.progress = (100 * value.loaded) / value.total + progressData.progress.next((100 * value.loaded) / value.total); } }) } @@ -28,7 +29,7 @@ export class BackgroundTaskService { } interface ProgressData { - progress: number + progress: BehaviorSubject } @Component({ @@ -37,10 +38,16 @@ interface ProgressData { styleUrls: ['./progress-indicator.snack-bar.scss'], standalone: true, imports: [ - MatSnackBarModule + MatSnackBarModule, + NgIf ] }) class SnackBarProgressIndicatorComponent { - constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ProgressData) { + constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ProgressData, private snackBarRef: MatSnackBarRef) { + data.progress.subscribe(value => { + if (value === 100) { + snackBarRef._dismissAfter(3000); + } + }) } } diff --git a/src/app/background-task/progress-indicator.snack-bar.html b/src/app/background-task/progress-indicator.snack-bar.html index 192e013..9c38733 100644 --- a/src/app/background-task/progress-indicator.snack-bar.html +++ b/src/app/background-task/progress-indicator.snack-bar.html @@ -1,3 +1,5 @@ -{{data.progress}}%: Database backup in progress... + {{ data.progress.getValue() }}%: Database backup + in progress... + finished! From 01f124d65c670631ab666ebb161c2f468efc08d8 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 7 Dec 2023 14:59:13 +0100 Subject: [PATCH 23/66] [us40] Remove unneeded TODO about moving showProgress to a pipe function --- src/app/database/database-backup-and-restore.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index db91600..7350fbc 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -27,8 +27,9 @@ export class DatabaseBackupAndRestoreService { return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); })); })); - // TODO: move this to a pipe function? + this.backgroundTaskService.showProgress(observable); + return observable; } From 2c1d89012a7c4cd6ec1966442f5fe6608d70fed9 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 7 Dec 2023 17:44:53 +0100 Subject: [PATCH 24/66] [us40] Show progress when restoring backup (missing http event progress report) --- .../background-task.service.spec.ts | 34 +++++------ .../background-task.service.ts | 53 ++++++++++++---- .../progress-indicator.snack-bar.html | 6 +- ...atabase-backup-and-restore.service.spec.ts | 28 +++++++-- .../database-backup-and-restore.service.ts | 61 ++++++++++++------- 5 files changed, 124 insertions(+), 58 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 9b1711c..3755711 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -2,9 +2,7 @@ import {BackgroundTaskService} from './background-task.service'; import {mock} from "strong-mock"; import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; -import {of} from "rxjs"; import {MatSnackBarModule} from "@angular/material/snack-bar"; -import {HttpEventType} from "@angular/common/http"; import {ComponentFixture, fakeAsync, tick} from "@angular/core/testing"; import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; import {HarnessLoader} from "@angular/cdk/testing"; @@ -32,11 +30,11 @@ describe('BackgroundTaskService', () => { const backgroundTaskService = fixture.point.componentInstance; // Act - backgroundTaskService.showProgress(of()); + backgroundTaskService.showProgress("Test", "Doing first test", 2); // Assert let result = await page.getProgressMessage(); - expect(result).toEqual("0%: Database backup in progress..."); + expect(result).toEqual("1/2 0% Test: Doing first test..."); }) it('Should show in progress', async () => { @@ -44,18 +42,19 @@ describe('BackgroundTaskService', () => { let fixture = MockRender(BackgroundTaskService); let page = new Page(fixture); const backgroundTaskService = fixture.point.componentInstance; + let progress = backgroundTaskService.showProgress("Test", "Doing first test", 3); // Act - backgroundTaskService.showProgress(of({ - type: HttpEventType.UploadProgress, - loaded: 50, - total: 200 - })); + progress.next({ + index: 2, + value: 25, + description: "Doing more test" + }) // Assert fixture.detectChanges(); let result = await page.getProgressMessage(); - expect(result).toEqual("25%: Database backup in progress..."); + expect(result).toEqual("2/3 25% Test: Doing more test..."); }) it('Should show as completed and should dismiss after 3s', fakeAsync(async () => { @@ -63,18 +62,18 @@ describe('BackgroundTaskService', () => { let fixture = MockRender(BackgroundTaskService); let page = new Page(fixture); const backgroundTaskService = fixture.point.componentInstance; + let progress = backgroundTaskService.showProgress("Test", "Doing first test", 2); // Act - backgroundTaskService.showProgress(of({ - type: HttpEventType.UploadProgress, - loaded: 200, - total: 200 - })); + progress.next({ + index: 2, + value: 100 + }) // Assert fixture.detectChanges(); let resultMessage = await page.getProgressMessage(); - expect(resultMessage).toEqual("100%: Database backup finished!"); + expect(resultMessage).toEqual("2/2 100% Test finished!"); tick(3000); // The message should be gone after 5 seconds at least resultMessage = await page.getProgressMessage(); @@ -105,7 +104,8 @@ export function mockBackgroundTaskService() { let backgroundTaskService = mock(); MockInstance(BackgroundTaskService, () => { return { - showProgress: backgroundTaskService.showProgress + showProgress: backgroundTaskService.showProgress, + updateProgress: backgroundTaskService.updateProgress } }); return backgroundTaskService; diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index d811ae3..403217a 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -1,8 +1,8 @@ import {Component, Inject, Injectable} from '@angular/core'; -import {BehaviorSubject, Observable} from "rxjs"; -import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; +import {BehaviorSubject} from "rxjs"; import {MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarModule, MatSnackBarRef} from "@angular/material/snack-bar"; import {NgIf} from "@angular/common"; +import {HttpProgressEvent, HttpResponse} from "@angular/common/http"; @Injectable({ providedIn: 'root' @@ -12,15 +12,25 @@ export class BackgroundTaskService { constructor(private snackBar: MatSnackBar) { } - showProgress(observable: Observable>): void { - let progressData: ProgressData = {progress: new BehaviorSubject(0)}; + showProgress(globalDescription: string, stepDescription: string, stepAmount: number): BehaviorSubject { + let progress = new BehaviorSubject({ + index: 1, + value: 0, + description: stepDescription, + }); + + let progressData: ProgressData = { + globalDescription: globalDescription, + stepAmount: stepAmount, + progress: progress + }; this.openSnackBar(progressData); - observable.subscribe(value => { - if (value.type === HttpEventType.UploadProgress && value.total) { - progressData.progress.next((100 * value.loaded) / value.total); - } - }) + return progress; + } + + updateProgress(progress: BehaviorSubject, httpEvent: HttpProgressEvent | HttpResponse) { + } private openSnackBar(data: ProgressData) { @@ -29,7 +39,21 @@ export class BackgroundTaskService { } interface ProgressData { - progress: BehaviorSubject + globalDescription: string; + stepAmount: number; + progress: BehaviorSubject +} + +export interface Progress { + index: number; + /** + * Percentage progress in percent, when it reaches 100, the associated message will be dismissed automatically + */ + value: number + /** + * No description when the tasks are finished + */ + description?: string, } @Component({ @@ -43,9 +67,14 @@ interface ProgressData { ] }) class SnackBarProgressIndicatorComponent { + progress: Progress; + constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ProgressData, private snackBarRef: MatSnackBarRef) { - data.progress.subscribe(value => { - if (value === 100) { + this.progress = data.progress.getValue(); + + data.progress.subscribe(progress => { + this.progress = progress; + if (progress.value === 100) { snackBarRef._dismissAfter(3000); } }) diff --git a/src/app/background-task/progress-indicator.snack-bar.html b/src/app/background-task/progress-indicator.snack-bar.html index 9c38733..9c6a261 100644 --- a/src/app/background-task/progress-indicator.snack-bar.html +++ b/src/app/background-task/progress-indicator.snack-bar.html @@ -1,5 +1,5 @@ - {{ data.progress.getValue() }}%: Database backup - in progress... - finished! + {{ progress.index }}/{{ data.stepAmount }} {{ progress.value }}% {{ data.globalDescription }} + : {{ progress.description }}... + finished! diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index eb9763c..e092031 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -10,8 +10,9 @@ import {mockFileElement} from "../file-list/file-list.component.spec"; import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; import {fakeAsync, TestBed, tick} from "@angular/core/testing"; import {db} from "./db"; -import {lastValueFrom} from "rxjs"; +import {BehaviorSubject, lastValueFrom} from "rxjs"; import {mockBackgroundTaskService} from "../background-task/background-task.service.spec"; +import {Progress} from "../background-task/background-task.service"; describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) @@ -32,12 +33,20 @@ describe('DatabaseBackupAndRestoreService', () => { }); describe('restore', () => { - it('the database should be automatically restored', fakeAsync(async () => { + it('The database should be automatically restored', fakeAsync(async () => { // Arrange let fileService = mockFileService(); let dbBackupFile = mockFileElement('db.backup'); when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([dbBackupFile])); + let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Automatic restore", "Downloading last backup", 2)) + .thenReturn(progress); + when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); + when(() => progress.next({index: 2, description: "Importing backup", value: 50})).thenReturn(); + when(() => progress.next({index: 2, value: 100})).thenReturn(); + let fixture = MockRender(DatabaseBackupAndRestoreService); let databaseBackupAndRestoreService = fixture.point.componentInstance; @@ -88,7 +97,7 @@ describe('DatabaseBackupAndRestoreService', () => { describe('backup', () => { - it('should upload a new backup file when there is no backup yet', async () => { + it('Should upload a new backup file when there is no backup yet', async () => { // Arrange let fileService = mockFileService(); when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([])); @@ -100,7 +109,11 @@ describe('DatabaseBackupAndRestoreService', () => { } as HttpResponse)); let backgroundTaskService = mockBackgroundTaskService(); - when(() => backgroundTaskService.showProgress(It.isAny())).thenReturn(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) + .thenReturn(progress); + when(() => progress.next({index: 2, description: "Uploading backup", value: 50})).thenReturn(); + when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -124,6 +137,13 @@ describe('DatabaseBackupAndRestoreService', () => { type: HttpEventType.Response } as HttpResponse)); + let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) + .thenReturn(progress); + when(() => progress.next({index: 2, description: "Uploading backup", value: 50})).thenReturn(); + when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); + const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; // Act diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 7350fbc..06cd72a 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -2,10 +2,10 @@ import {Injectable} from '@angular/core'; import {exportDB, importDB} from "dexie-export-import"; import {db} from "./db"; import {FileUploadService} from "../file-upload/file-upload.service"; -import {from, map, mergeMap, Observable, of} from "rxjs"; +import {filter, from, last, map, mergeMap, Observable, of, tap} from "rxjs"; import {FileService} from "../file-list/file.service"; import {FileElement, isFileElement} from "../file-list/file-list.component"; -import {HttpClient} from "@angular/common/http"; +import {HttpClient, HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; import {BackgroundTaskService} from "../background-task/background-task.service"; @Injectable({ @@ -20,29 +20,46 @@ export class DatabaseBackupAndRestoreService { } backup() { - let observable = from(exportDB(db)) - .pipe(mergeMap(blob => { - return this.findExistingDbFile() - .pipe(mergeMap(dbFile => { - return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); - })); - })); - - this.backgroundTaskService.showProgress(observable); - - return observable; + let progress = this.backgroundTaskService.showProgress('Backup', + "Creating backup", 2); + return from(exportDB(db)) + .pipe(tap(() => progress.next({index: 2, value: 50, description: "Uploading backup"})), + mergeMap(blob => { + return this.findExistingDbFile() + .pipe(mergeMap(dbFile => { + return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); + })); + }), + tap(httpEvent => this.backgroundTaskService.updateProgress(progress, httpEvent))); } restore(): Observable { - return this.findExistingDbFile().pipe(mergeMap(dbFile => { - if (dbFile) { - let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + dbFile.id + '?alt=media'; - return this.http.get(dlLink, {responseType: "blob"}); - } - return of(); - }), mergeMap(dbDownloadResponse => { - return from(importDB(dbDownloadResponse)); - }), map(() => void 0)); + let progress = this.backgroundTaskService.showProgress('Automatic restore', + "Downloading last backup", 2); + return this.findExistingDbFile().pipe( + tap(() => progress.next({index: 2, value: 50, description: 'Importing backup'})), + mergeMap(dbFile => { + if (dbFile) { + let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + dbFile.id + '?alt=media'; + return this.http.get(dlLink, {responseType: "blob", observe: "events", reportProgress: true}); + } + return of(); + }), + filter((e: HttpEvent): e is HttpProgressEvent | HttpResponse => e.type === HttpEventType.DownloadProgress || e.type === HttpEventType.Response), + tap(event => this.backgroundTaskService.updateProgress(progress, event)), + last(), + mergeMap(event => { + if (event.type === HttpEventType.Response && event.body) { + return of(event.body); + } else { + return of(); + } + }), + mergeMap(dbDownloadResponse => { + return from(importDB(dbDownloadResponse)); + }), + tap(() => progress.next({index: 2, value: 100})), + map(() => void 0)); } private findExistingDbFile() { From 3a0bcef0f849998301957d3f56377e96e13f7762 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 7 Dec 2023 18:08:16 +0100 Subject: [PATCH 25/66] [us40] Support showing http event progress report --- .../background-task.service.spec.ts | 105 +++++++++++++++--- .../background-task.service.ts | 17 ++- 2 files changed, 102 insertions(+), 20 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 3755711..c3f59dc 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -1,12 +1,12 @@ -import {BackgroundTaskService} from './background-task.service'; +import {BackgroundTaskService, Progress} from './background-task.service'; import {mock} from "strong-mock"; import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {MatSnackBarModule} from "@angular/material/snack-bar"; -import {ComponentFixture, fakeAsync, tick} from "@angular/core/testing"; +import {fakeAsync, tick} from "@angular/core/testing"; import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; -import {HarnessLoader} from "@angular/cdk/testing"; -import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; +import {BehaviorSubject} from "rxjs"; +import {HttpDownloadProgressEvent, HttpEventType, HttpResponse, HttpUploadProgressEvent} from "@angular/common/http"; describe('BackgroundTaskService', () => { beforeEach(() => MockBuilder(BackgroundTaskService, AppModule) @@ -26,21 +26,19 @@ describe('BackgroundTaskService', () => { it('Should show initial 0 progress', async () => { // Arrange let fixture = MockRender(BackgroundTaskService); - let page = new Page(fixture); const backgroundTaskService = fixture.point.componentInstance; // Act backgroundTaskService.showProgress("Test", "Doing first test", 2); // Assert - let result = await page.getProgressMessage(); + let result = await Page.getProgressMessage(); expect(result).toEqual("1/2 0% Test: Doing first test..."); }) it('Should show in progress', async () => { // Arrange let fixture = MockRender(BackgroundTaskService); - let page = new Page(fixture); const backgroundTaskService = fixture.point.componentInstance; let progress = backgroundTaskService.showProgress("Test", "Doing first test", 3); @@ -53,14 +51,14 @@ describe('BackgroundTaskService', () => { // Assert fixture.detectChanges(); - let result = await page.getProgressMessage(); + let result = await Page.getProgressMessage(); expect(result).toEqual("2/3 25% Test: Doing more test..."); }) it('Should show as completed and should dismiss after 3s', fakeAsync(async () => { // Arrange let fixture = MockRender(BackgroundTaskService); - let page = new Page(fixture); + let page = new Page(); const backgroundTaskService = fixture.point.componentInstance; let progress = backgroundTaskService.showProgress("Test", "Doing first test", 2); @@ -72,25 +70,96 @@ describe('BackgroundTaskService', () => { // Assert fixture.detectChanges(); - let resultMessage = await page.getProgressMessage(); + let resultMessage = await Page.getProgressMessage(); expect(resultMessage).toEqual("2/2 100% Test finished!"); tick(3000); // The message should be gone after 5 seconds at least - resultMessage = await page.getProgressMessage(); + resultMessage = await Page.getProgressMessage(); expect(resultMessage).toEqual(undefined); })) }) -}); + describe('updateProgress', () => { + it('Should update progress with intermediate download progress event', () => { + // Arrange + const backgroundTaskService = MockRender(BackgroundTaskService).point.componentInstance; + let progress = new BehaviorSubject({ + index: 2, + value: 0, + description: "Testing download" + }); -class Page { - private loader: HarnessLoader; + let httpEvent: HttpDownloadProgressEvent = { + type: HttpEventType.DownloadProgress, + loaded: 50, + total: 200 + }; - constructor(fixture: ComponentFixture) { - this.loader = TestbedHarnessEnvironment.documentRootLoader(fixture); - } + // Act + backgroundTaskService.updateProgress(progress, httpEvent) + + // Assert + expect(progress.getValue()).toEqual({ + index: 2, + value: 25, + description: "Testing download" + }); + }); + + it('Should update progress with intermediate upload progress event', () => { + // Arrange + const backgroundTaskService = MockRender(BackgroundTaskService).point.componentInstance; + let progress = new BehaviorSubject({ + index: 2, + value: 0, + description: "Testing download" + }); + + let httpEvent: HttpUploadProgressEvent = { + type: HttpEventType.UploadProgress, + loaded: 50, + total: 200 + }; + + // Act + backgroundTaskService.updateProgress(progress, httpEvent) - async getProgressMessage() { + // Assert + expect(progress.getValue()).toEqual({ + index: 2, + value: 25, + description: "Testing download" + }); + }); + + it('Should update progress with response event', () => { + // Arrange + const backgroundTaskService = MockRender(BackgroundTaskService).point.componentInstance; + let progress = new BehaviorSubject({ + index: 2, + value: 0, + description: "Testing download" + }); + + let httpEvent: HttpResponse = { + type: HttpEventType.Response, + } as HttpResponse; + + // Act + backgroundTaskService.updateProgress(progress, httpEvent) + + // Assert + expect(progress.getValue()).toEqual({ + index: 2, + value: 100, + description: "Testing download" + }); + }); + }) +}); + +class Page { + static async getProgressMessage() { let element = document.body.querySelector('mat-snack-bar-container'); if (element) { let textContent = element.textContent; diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index 403217a..c911fef 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -2,7 +2,7 @@ import {Component, Inject, Injectable} from '@angular/core'; import {BehaviorSubject} from "rxjs"; import {MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarModule, MatSnackBarRef} from "@angular/material/snack-bar"; import {NgIf} from "@angular/common"; -import {HttpProgressEvent, HttpResponse} from "@angular/common/http"; +import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; @Injectable({ providedIn: 'root' @@ -30,7 +30,20 @@ export class BackgroundTaskService { } updateProgress(progress: BehaviorSubject, httpEvent: HttpProgressEvent | HttpResponse) { - + let lastProgress = progress.getValue(); + if ((httpEvent.type === HttpEventType.DownloadProgress || httpEvent.type === HttpEventType.UploadProgress) && httpEvent.total) { + progress.next({ + index: lastProgress.index, + value: (httpEvent.loaded * 100) / httpEvent.total, + description: lastProgress.description + }) + } else if (httpEvent.type === HttpEventType.Response) { + progress.next({ + index: lastProgress.index, + value: 100, + description: lastProgress.description + }) + } } private openSnackBar(data: ProgressData) { From b0e3004c65023871d1639524f1052325d45c5b0f Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 22 Dec 2023 16:27:34 +0100 Subject: [PATCH 26/66] [us40] Pre-load once files list on page load to avoid multiple api calls and to simplify methods --- src/app/app-routing.module.spec.ts | 43 +-- src/app/app-routing.module.ts | 25 +- src/app/app.module.ts | 8 +- src/app/auth/auth.guard.spec.ts | 11 +- src/app/file-list/file-list.component.spec.ts | 286 ++++++++---------- src/app/file-list/file-list.component.ts | 79 ++--- src/app/file-list/file.service.ts | 1 + src/app/homepage/homepage.component.html | 4 +- src/app/homepage/homepage.component.spec.ts | 4 +- src/app/resolver/files.resolver.spec.ts | 57 ++++ src/app/resolver/files.resolver.ts | 25 ++ src/app/route-strategy.service.ts | 17 ++ src/app/rules/rules.component.html | 2 +- src/app/user-root/user-root.component.html | 1 + src/app/user-root/user-root.component.scss | 0 src/app/user-root/user-root.component.spec.ts | 12 + src/app/user-root/user-root.component.ts | 34 +++ src/testing/common-testing-function.spec.ts | 22 ++ 18 files changed, 389 insertions(+), 242 deletions(-) create mode 100644 src/app/resolver/files.resolver.spec.ts create mode 100644 src/app/resolver/files.resolver.ts create mode 100644 src/app/route-strategy.service.ts create mode 100644 src/app/user-root/user-root.component.html create mode 100644 src/app/user-root/user-root.component.scss create mode 100644 src/app/user-root/user-root.component.spec.ts create mode 100644 src/app/user-root/user-root.component.ts diff --git a/src/app/app-routing.module.spec.ts b/src/app/app-routing.module.spec.ts index a05167d..3b65e25 100644 --- a/src/app/app-routing.module.spec.ts +++ b/src/app/app-routing.module.spec.ts @@ -1,23 +1,27 @@ -import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; -import {Router, RouterModule, RouterOutlet} from "@angular/router"; +import {MockBuilder, MockInstance, NG_MOCKS_RESOLVERS, ngMocks} from "ng-mocks"; +import {RouterModule} from "@angular/router"; import {RouterTestingModule} from "@angular/router/testing"; import {AppModule} from "./app.module"; -import {Location} from '@angular/common'; -import {fakeAsync, tick} from "@angular/core/testing"; +import {fakeAsync} from "@angular/core/testing"; import {RulesComponent} from "./rules/rules.component"; import {GoogleDriveAuthService} from "./file-upload/google-drive-auth.service"; import {mock, when} from "strong-mock"; import {HomepageComponent} from "./homepage/homepage.component"; +import {UserRootComponent} from "./user-root/user-root.component"; +import {navigateTo} from "../testing/common-testing-function.spec"; describe('AppRoutingModule', () => { beforeEach(() => { return MockBuilder( [ RouterModule, - RouterTestingModule.withRoutes([]) + RouterTestingModule.withRoutes([]), ], AppModule, - ); + ) + // We use the real UserRootComponent as we need it to load its children route + .keep(UserRootComponent) + .exclude(NG_MOCKS_RESOLVERS) }); describe('when logged in', () => { beforeEach(() => { @@ -32,40 +36,19 @@ describe('AppRoutingModule', () => { }) it('should display rules page', fakeAsync(() => { - // Arrange - const fixture = MockRender(RouterOutlet, {}); - const router: Router = fixture.point.injector.get(Router); - const location: Location = fixture.point.injector.get(Location); - // Act - location.go('/rules'); + navigateTo('/rules'); // Assert - if (fixture.ngZone) { - fixture.ngZone.run(() => router.initialNavigation()); - tick(); // is needed for rendering of the current route. - } - - expect(location.path()).toEqual('/rules'); + expect(() => ngMocks.find(UserRootComponent)).not.toThrow(); expect(() => ngMocks.find(RulesComponent)).not.toThrow(); })); it('should display home page', fakeAsync(() => { - // Arrange - const fixture = MockRender(RouterOutlet, {}); - const router: Router = fixture.point.injector.get(Router); - const location: Location = fixture.point.injector.get(Location); - // Act - location.go('/'); + navigateTo("/") // Assert - if (fixture.ngZone) { - fixture.ngZone.run(() => router.initialNavigation()); - tick(); // is needed for rendering of the current route. - } - - expect(location.path()).toEqual('/'); expect(() => ngMocks.find(HomepageComponent)).not.toThrow(); })); }) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index b9db162..042dde5 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,17 +4,28 @@ import {HomepageComponent} from "./homepage/homepage.component"; import {authGuard} from "./auth/auth.guard"; import {LoginComponent} from "./login/login.component"; import {RulesComponent} from "./rules/rules.component"; +import {filesResolver} from "./resolver/files.resolver"; +import {UserRootComponent} from "./user-root/user-root.component"; const routes: Routes = [ { path: '', - component: HomepageComponent, - canActivate: [authGuard] - }, - { - path: 'rules', - component: RulesComponent, - canActivate: [authGuard] + component: UserRootComponent, + canActivate: [authGuard], + resolve: {files: filesResolver}, + runGuardsAndResolvers: () => { + return UserRootComponent.shouldReloadRouteData(); + }, + children: [ + { + path: 'rules', + component: RulesComponent + }, + { + path: '', + component: HomepageComponent, + } + ] }, { path: 'login', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 444204c..1aa4075 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -34,6 +34,8 @@ import {MatSortModule} from "@angular/material/sort"; import {RulesComponent} from './rules/rules.component'; import {MatExpansionModule} from "@angular/material/expansion"; import {MatSnackBarModule} from "@angular/material/snack-bar"; +import {UserRootComponent} from './user-root/user-root.component'; +import {routeReuseStrategyProvider} from "./route-strategy.service"; @NgModule({ declarations: [ @@ -46,7 +48,8 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; FileListComponent, LoginComponent, TitleHeaderComponent, - RulesComponent + RulesComponent, + UserRootComponent ], imports: [ BrowserModule, @@ -75,7 +78,8 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; MatSnackBarModule ], providers: [ - httpInterceptorProviders + httpInterceptorProviders, + routeReuseStrategyProvider ], bootstrap: [AppComponent] }) diff --git a/src/app/auth/auth.guard.spec.ts b/src/app/auth/auth.guard.spec.ts index cc89a50..682d5d9 100644 --- a/src/app/auth/auth.guard.spec.ts +++ b/src/app/auth/auth.guard.spec.ts @@ -1,5 +1,13 @@ import {authGuard} from './auth.guard'; -import {MockBuilder, MockedComponentFixture, MockInstance, MockRender, NG_MOCKS_GUARDS, ngMocks} from "ng-mocks"; +import { + MockBuilder, + MockedComponentFixture, + MockInstance, + MockRender, + NG_MOCKS_GUARDS, + NG_MOCKS_RESOLVERS, + ngMocks +} from "ng-mocks"; import {Router, RouterModule, RouterOutlet} from "@angular/router"; import {RouterTestingModule} from "@angular/router/testing"; import {AppModule} from "../app.module"; @@ -26,6 +34,7 @@ describe('authGuard', () => { ) // excluding all guards to avoid side effects .exclude(NG_MOCKS_GUARDS) + .exclude(NG_MOCKS_RESOLVERS) // keeping guard for testing .keep(authGuard) ); diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 90acca2..eb4c76b 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -1,11 +1,16 @@ import {fakeAsync, tick} from '@angular/core/testing'; -import {FileElement, FileListComponent, FolderElement, SelectFileCategoryDialog} from './file-list.component'; -import {MockBuilder, MockedComponentFixture, MockedDebugElement, MockInstance, MockRender, ngMocks} from "ng-mocks"; +import { + FileElement, + FileListComponent, + FolderElement, + isFileElement, + SelectFileCategoryDialog +} from './file-list.component'; +import {MockBuilder, MockedComponentFixture, MockedDebugElement, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; import {MatTableModule} from "@angular/material/table"; import {mock, when} from "strong-mock"; -import {FileService} from "./file.service"; import {of} from "rxjs"; import {NgxFilesizeModule} from "ngx-filesize"; import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; @@ -29,18 +34,34 @@ import {MatAutocompleteHarness} from "@angular/material/autocomplete/testing"; import {MatChipGridHarness} from "@angular/material/chips/testing"; import {mockFileService} from "./file.service.spec"; import {MatSortModule} from "@angular/material/sort"; -import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; import {BreakpointObserver} from "@angular/cdk/layout"; +import {FilesCache} from "../resolver/files.resolver"; +import {UserRootComponent} from "../user-root/user-root.component"; function mockRenderAndWaitForChanges() { - let fixture = MockRender(FileListComponent); - tick(); + let fixture = MockRender(FileListComponent, null, {reset: true}); + try { + tick(); + } catch (e) { + } fixture.detectChanges(); return fixture; } +function mockFilesCacheInRouteData() { + let filesCache: FilesCache = { + baseFolder: 'baseFolderId', + all: [] + }; + let userRootComponent = ngMocks.findInstance(UserRootComponent); + when(() => userRootComponent.getFilesCache()).thenReturn(filesCache); + + return filesCache; +} + describe('FileListComponent', () => { beforeEach(() => MockBuilder(FileListComponent, AppModule) + .mock(UserRootComponent) .keep(MatTableModule) .keep(NgxFilesizeModule) .keep(MatMenuModule) @@ -51,28 +72,29 @@ describe('FileListComponent', () => { .keep(MatChipsModule) .keep(MatSortModule) .keep(BreakpointObserver) + // For some reason, we need to explicitly add a provider for UserRootComponent + .provide({ + provide: UserRootComponent, + useValue: mock() + }) .replace(BrowserAnimationsModule, NoopAnimationsModule) ); it('should create (no element)', fakeAsync(() => { // Arrange - mockBaseFolderService(); - - let listMock = MockInstance(FileService, 'findAll', mock()); - when(() => listMock()).thenReturn(mustBeConsumedAsyncObservable([])); + mockFilesCacheInRouteData(); // Act - const component = MockRender(FileListComponent).point.componentInstance; + const component = mockRenderAndWaitForChanges().point.componentInstance; // Assert - tick(); expect(component).toBeTruthy(); expect(Page.getTableRows()).toEqual([]); })); it('should list two items', fakeAsync(() => { // Arrange - mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); + mockListItemsAndCategoriesWithTwoItemsAndTwoCategories(); // Act mockRenderAndWaitForChanges() @@ -108,7 +130,7 @@ describe('FileListComponent', () => { itemsAndCategories.push(ab5Cat); itemsAndCategories.push(mockFolderElement('cd5', ab5Cat.id)); itemsAndCategories.push(mockFolderElement('cd4', ab5Cat.id)); - mockListItemsAndCategories(itemsAndCategories); + mockListItemsAndCategories(itemsAndCategories, true); // Act mockRenderAndWaitForChanges(); @@ -119,59 +141,41 @@ describe('FileListComponent', () => { it('should trash an item then refresh', fakeAsync(async () => { // Arrange - let fileService = mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); - when(() => fileService.trash('id2')) + let cat1 = mockFolderElement('Cat1'); + let el1 = mockFileElement('name1', cat1.id); + let fileService = mockListItemsAndCategories([el1, cat1]); + when(() => fileService.trash(el1.id)) .thenReturn(mustBeConsumedAsyncObservable(undefined)); - let el1: FileElement = { - id: 'id1', - size: 1421315, - date: '2023-08-14T14:48:44.928Z', - name: 'name1', - iconLink: "link", - dlLink: "dlLink", - parentId: "rootId" - }; - when(() => fileService.findAll()).thenReturn(of([el1])) + // A refresh is expected + let userRootComponent = ngMocks.get(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act - Page.openItemMenu('name2'); + Page.openItemMenu('name1'); await page.clickMenuTrash() // Assert - tick(); - let actionsRow = 'more_vert'; - let expected = [['name1', '', 'Aug 14, 2023, 2:48:44 PM', '1.42 MB', actionsRow]]; - expect(Page.getTableRows()).toEqual(expected); + // No failure in mock setup })) - it('When trashing the last file from a category, should also remove the category', fakeAsync(async () => { + it('When a category is empty, should automatically remove it', fakeAsync(async () => { // Arrange let cat1Folder = mockFolderElement('Cat1'); - let fileElement = mockFileElement('name1', cat1Folder.id); + let fileElement = mockFileElement('name1'); let fileService = mockListItemsAndCategories([fileElement, cat1Folder]); - // We expect a refresh, the refresh should include the folder and the file which have moved - let fileElementAfterRefresh = mockFileElement('name1'); - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([fileElementAfterRefresh, cat1Folder])); - - when(() => fileService.trash(fileElement.id)) - .thenReturn(mustBeConsumedAsyncObservable(undefined)); // We expect the category to be trashed since there is no file in it anymore when(() => fileService.trash(cat1Folder.id)) .thenReturn(mustBeConsumedAsyncObservable(undefined)); - // We expect a last refresh after trashing the category - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([fileElementAfterRefresh])); - - let fixture = mockRenderAndWaitForChanges(); - let page = new Page(fixture); + let userRootComponent = ngMocks.findInstance(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); // Act - Page.openItemMenu('name1'); - await page.clickMenuTrash(); + mockRenderAndWaitForChanges(); // Assert // No failure from mock setup @@ -179,10 +183,13 @@ describe('FileListComponent', () => { it('should list two categories and one sub-category', fakeAsync(() => { // Arrange - mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); + let cat1 = mockFolderElement('Cat1'); + let cat1Child = mockFolderElement('Cat1Child', cat1.id); + let cat2 = mockFolderElement('Cat2'); + mockListItemsAndCategories([cat1, cat1Child, cat2], true); // Act - MockRender(FileListComponent) + mockRenderAndWaitForChanges(); // Assert tick(); @@ -191,12 +198,13 @@ describe('FileListComponent', () => { it('should not list base folder as category in row categories', fakeAsync(() => { // Arrange - let baseFolder = mockFolderElement('BaseFolder', 'rootId', 'baseFolderId'); - let el1 = mockFileElement('name1', baseFolder.id, 'id1', 1421315, '2023-08-14T14:48:44.928Z'); + let baseFolder = mockFolderElement('BaseFolder', 'rootId'); + baseFolder.id = 'baseFolderId' + let el1 = mockFileElement('name1', baseFolder.id, 1421315, '2023-08-14T14:48:44.928Z'); mockListItemsAndCategories([baseFolder, el1]); // Act - let fixture = mockRenderAndWaitForChanges(); + mockRenderAndWaitForChanges(); // Assert let actionsRow = 'more_vert'; @@ -210,20 +218,12 @@ describe('FileListComponent', () => { let el2 = mockFileElement('name2'); let fileService = mockListItemsAndCategories([el2]); - let el1: FileElement = { - id: 'id1', - size: 1421315, - date: '2023-08-14T14:48:44.928Z', - name: 'name1', - iconLink: "link", - dlLink: "dlLink", - parentId: "rootId" - }; - when(() => fileService.findAll()).thenReturn(of([el1])) - let findOrCreateFolderMock = MockInstance(FileService, 'findOrCreateFolder', mock()); - when(() => findOrCreateFolderMock('Cat848', 'baseFolderId')).thenReturn(of('cat848Id')); - let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); - when(() => setCategoryMock(el2.id, 'cat848Id')).thenReturn(of(undefined)); + // A refresh is expected + let userRootComponent = ngMocks.get(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + + when(() => fileService.findOrCreateFolder('Cat848', 'baseFolderId')).thenReturn(of('cat848Id')); + when(() => fileService.setCategory(el2.id, 'cat848Id')).thenReturn(of(undefined)); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -235,15 +235,12 @@ describe('FileListComponent', () => { await page.clickOkInDialog(); // Assert - tick(); - let actionsRow = 'more_vert'; - let expected = [['name1', '', 'Aug 14, 2023, 2:48:44 PM', '1.42 MB', actionsRow]]; - expect(Page.getTableRows()).toEqual(expected); + // No failure in mock setup })) it('should show name of the file being assigned to a category in dialog', fakeAsync(async () => { // Arrange - mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); + mockListItemsAndCategoriesWithTwoItemsAndTwoCategories(); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -262,7 +259,7 @@ describe('FileListComponent', () => { it('should cancel when clicking on cancel', fakeAsync(async () => { // Arrange - mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); + mockListItemsAndCategoriesWithTwoItemsAndTwoCategories(); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -283,11 +280,11 @@ describe('FileListComponent', () => { // Arrange let fileElement = mockFileElement('name1'); let fileService = mockListItemsAndCategories([fileElement]); - // We expect a refresh - when(() => fileService.findAll()).thenReturn(of()); + // A refresh is expected + let userRootComponent = ngMocks.get(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); - let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); - when(() => setCategoryMock(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); + when(() => fileService.setCategory(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -307,11 +304,11 @@ describe('FileListComponent', () => { // Arrange let fileElement = mockFileElement('name1'); let fileService = mockListItemsAndCategories([fileElement]); - // We expect a refresh - when(() => fileService.findAll()).thenReturn(of()); + // A refresh is expected + let userRootComponent = ngMocks.get(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); - let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); - when(() => setCategoryMock(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); + when(() => fileService.setCategory(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -331,14 +328,13 @@ describe('FileListComponent', () => { // Arrange let fileElement = mockFileElement('name1'); let fileService = mockListItemsAndCategories([fileElement]); - // We expect a refresh - when(() => fileService.findAll()).thenReturn(of()); + // A refresh is expected + let userRootComponent = ngMocks.get(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); - let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); - when(() => setCategoryMock(fileElement.id, "parentCat45Id")).thenReturn(of(undefined)); + when(() => fileService.setCategory(fileElement.id, "parentCat45Id")).thenReturn(of(undefined)); - let findOrCreateFolderMock = MockInstance(FileService, 'findOrCreateFolder', mock()); - when(() => findOrCreateFolderMock('Cat45', 'baseFolderId')).thenReturn(of('parentCat45Id')); + when(() => fileService.findOrCreateFolder('Cat45', 'baseFolderId')).thenReturn(of('parentCat45Id')); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -357,15 +353,14 @@ describe('FileListComponent', () => { // Arrange let el2 = mockFileElement('name2'); let fileService = mockListItemsAndCategories([el2]); - // We expect a refresh - when(() => fileService.findAll()).thenReturn(of()); + // A refresh is expected + let userRootComponent = ngMocks.get(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); - let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); - when(() => setCategoryMock(el2.id, 'cat7Id')).thenReturn(of(undefined)); + when(() => fileService.setCategory(el2.id, 'cat7Id')).thenReturn(of(undefined)); - let findOrCreateFolderMock = MockInstance(FileService, 'findOrCreateFolder', mock()); - when(() => findOrCreateFolderMock('ParentCat8', 'baseFolderId')).thenReturn(of('parentCat8Id')); - when(() => findOrCreateFolderMock('Cat7', 'parentCat8Id')).thenReturn(of('cat7Id')); + when(() => fileService.findOrCreateFolder('ParentCat8', 'baseFolderId')).thenReturn(of('parentCat8Id')); + when(() => fileService.findOrCreateFolder('Cat7', 'parentCat8Id')).thenReturn(of('cat7Id')); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -387,7 +382,7 @@ describe('FileListComponent', () => { let cat2Folder = mockFolderElement('cat2'); let cat1bFolder = mockFolderElement('cat1b', cat1Folder.id); let fileElement1 = mockFileElement('name1'); - mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1]); + mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1], true); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -407,7 +402,7 @@ describe('FileListComponent', () => { let cat2Folder = mockFolderElement('cat2'); let cat1bFolder = mockFolderElement('cat1b', cat1Folder.id); let fileElement1 = mockFileElement('name1'); - mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1]); + mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1], true); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -428,16 +423,15 @@ describe('FileListComponent', () => { let cat2Folder = mockFolderElement('cat2'); let cat1bFolder = mockFolderElement('cat1b', cat1Folder.id); let fileElement1 = mockFileElement('name1'); - let fileService = mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1]); + let fileService = mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1], true); - // We expect a refresh - when(() => fileService.findAll()).thenReturn(of([])); + // A refresh is expected + let userRootComponent = ngMocks.get(UserRootComponent); + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); - let setCategoryMock = MockInstance(FileService, 'setCategory', mock()); - when(() => setCategoryMock(fileElement1.id, cat1Folder.id)).thenReturn(of(undefined)); + when(() => fileService.setCategory(fileElement1.id, cat1Folder.id)).thenReturn(of(undefined)); - let findOrCreateFolderMock = MockInstance(FileService, 'findOrCreateFolder', mock()); - when(() => findOrCreateFolderMock(cat1Folder.name, 'baseFolderId')).thenReturn(of(cat1Folder.id)); + when(() => fileService.findOrCreateFolder(cat1Folder.name, 'baseFolderId')).thenReturn(of(cat1Folder.id)); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -459,7 +453,7 @@ describe('FileListComponent', () => { let cat2Folder = mockFolderElement('cat2'); let cat1bFolder = mockFolderElement('cat1b', cat1Folder.id); let fileElement1 = mockFileElement('name1'); - mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1]); + mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1], true); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -479,7 +473,7 @@ describe('FileListComponent', () => { // Arrange let cat1Folder = mockFolderElement('cat1'); let fileElement1 = mockFileElement('name1'); - mockListItemsAndCategories([cat1Folder, fileElement1]); + mockListItemsAndCategories([cat1Folder, fileElement1], true); let fixture = mockRenderAndWaitForChanges() let page = new Page(fixture); @@ -499,7 +493,7 @@ describe('FileListComponent', () => { let cat1Folder = mockFolderElement('cat1'); let cat1bFolder = mockFolderElement('cat1b', cat1Folder.id); let fileElement = mockFileElement('name1'); - mockListItemsAndCategories([cat1Folder, cat1bFolder, fileElement]); + mockListItemsAndCategories([cat1Folder, cat1bFolder, fileElement], true); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -536,44 +530,13 @@ describe('FileListComponent', () => { let result = await page.getCategoriesInDialog(); expect(result).toEqual(['cat1', 'cat1b']) })) - - it('When moving the last file from a category, should also remove the category', fakeAsync(async () => { - // Arrange - let cat1Folder = mockFolderElement('Cat1'); - let fileElement = mockFileElement('name1', cat1Folder.id); - let fileService = mockListItemsAndCategories([fileElement, cat1Folder]); - // We expect a refresh, the refresh should include the folder and the file which have moved - let fileElementAfterRefresh = mockFileElement('name1'); - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([fileElementAfterRefresh, cat1Folder])); - - when(() => fileService.setCategory(fileElement.id, "baseFolderId")).thenReturn(mustBeConsumedAsyncObservable(undefined)); - - let trashObservable = mustBeConsumedAsyncObservable(undefined); - when(() => fileService.trash(cat1Folder.id)) - .thenReturn(trashObservable); - // After removing the category, we expect another refresh - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([fileElementAfterRefresh], trashObservable)); - - let fixture = mockRenderAndWaitForChanges(); - let page = new Page(fixture); - - // Act - Page.openItemMenu('name1'); - // We need to remove the existing category - await page.clickMenuAssignCategory(); - await page.removeCategoryInDialog('Cat1'); - await page.clickOkInDialog(); - - // Assert - // No failure from mock setup - })) }) describe('Filter by file name', () => { it('should filter out one item out of two items', async () => { // Arrange - mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); - let fixture = MockRender(FileListComponent); + mockListItemsAndCategoriesWithTwoItemsAndTwoCategories(); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -589,7 +552,7 @@ describe('FileListComponent', () => { // Arrange let el1 = mockFileElement('nAme1'); mockListItemsAndCategories([el1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -603,7 +566,7 @@ describe('FileListComponent', () => { // Arrange let el1 = mockFileElement('name1'); mockListItemsAndCategories([el1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -617,7 +580,7 @@ describe('FileListComponent', () => { // Arrange let el1 = mockFileElement('name1'); mockListItemsAndCategories([el1]); - let fixture = MockRender(FileListComponent); + let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); // Act @@ -632,7 +595,7 @@ describe('FileListComponent', () => { describe('Filter by file category', () => { it('should filter out one item out of two items', fakeAsync(() => { // Arrange - mockListItemsAndCategoriesWithTwoItemsAndThreeCategories(); + mockListItemsAndCategoriesWithTwoItemsAndTwoCategories(); let fixture = mockRenderAndWaitForChanges(); // Act @@ -760,10 +723,8 @@ describe('FileListComponent', () => { }) }); -export function mockFileElement(name: string, parentId: string = 'baseFolderId', id: string | undefined = undefined, size: number = 0, date: string = ''): FileElement { - if (!id) { - id = name + '-' + uuid(); - } +export function mockFileElement(name: string, parentId: string = 'baseFolderId', size: number = 0, date: string = ''): FileElement { + let id = name + '-' + uuid(); return { id: id, size: size, @@ -775,10 +736,8 @@ export function mockFileElement(name: string, parentId: string = 'baseFolderId', }; } -function mockFolderElement(name: string, parentId: string = 'baseFolderId', id: string | undefined = undefined): FolderElement { - if (!id) { - id = name + '-' + uuid(); - } +function mockFolderElement(name: string, parentId: string = 'baseFolderId'): FolderElement { + let id = name + '-' + uuid(); return { id: id, date: '2023-08-02T14:54:55.556Z', @@ -788,11 +747,17 @@ function mockFolderElement(name: string, parentId: string = 'baseFolderId', id: } } -function mockListItemsAndCategories(itemsAndCategories: (FileElement | FolderElement)[]) { - mockBaseFolderService(); - let fileServiceMock = mockFileService(); - when(() => fileServiceMock.findAll()).thenReturn(of(itemsAndCategories)); - return fileServiceMock; +function mockListItemsAndCategories(itemsAndCategories: (FileElement | FolderElement)[], fillEachCategory: boolean = false) { + let filesCache = mockFilesCacheInRouteData(); + if (fillEachCategory) { + let categories = itemsAndCategories.filter(file => !isFileElement(file)) + .map(value => value as FolderElement); + categories.forEach(cat => { + itemsAndCategories.push(mockFileElement(cat.name + "_file", cat.id)) + }) + } + filesCache.all = itemsAndCategories; + return mockFileService(); } function mockTxtAndImageFiles() { @@ -811,13 +776,12 @@ function mockTxtAndImageFiles() { /** * @return two files, two categories and one sub-category */ -function mockListItemsAndCategoriesWithTwoItemsAndThreeCategories() { - let el3 = mockFolderElement('Cat1', 'baseFolderId', 'id3'); - let el4 = mockFolderElement('Cat2', 'baseFolderId', 'id4'); - let el5 = mockFolderElement('Cat1Child', el3.id, 'id5'); - let el1 = mockFileElement('name1', el3.id, 'id1', 1421315, '2023-08-14T14:48:44.928Z'); - let el2 = mockFileElement('name2', el5.id, 'id2', 1745, '2023-08-03T14:54:55.556Z'); - let itemsAndCategories = [el1, el2, el3, el4, el5]; +function mockListItemsAndCategoriesWithTwoItemsAndTwoCategories() { + let cat1 = mockFolderElement('Cat1', 'baseFolderId'); + let cat1Child = mockFolderElement('Cat1Child', cat1.id); + let el1 = mockFileElement('name1', cat1.id, 1421315, '2023-08-14T14:48:44.928Z'); + let el2 = mockFileElement('name2', cat1Child.id, 1745, '2023-08-03T14:54:55.556Z'); + let itemsAndCategories = [el1, el2, cat1, cat1Child]; return mockListItemsAndCategories(itemsAndCategories); } diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index ed60e8b..3ce0ffb 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -1,7 +1,6 @@ import {Component, ElementRef, Inject, OnInit, ViewChild} from '@angular/core'; import {MatTableDataSource} from "@angular/material/table"; import {FileService} from "./file.service"; -import {BaseFolderService} from "../file-upload/base-folder.service"; import {MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef} from "@angular/material/dialog"; import {MatFormFieldModule} from "@angular/material/form-field"; import {MatInputModule} from "@angular/material/input"; @@ -20,6 +19,7 @@ import { MatAutocompleteTrigger } from "@angular/material/autocomplete"; import {MatSort, MatSortable} from "@angular/material/sort"; +import {UserRootComponent} from "../user-root/user-root.component"; export interface FileOrFolderElement { id: string; @@ -62,41 +62,38 @@ export class FileListComponent implements OnInit { @ViewChild(MatSort, {static: true}) fileSort?: MatSort; isCategoryPanelExpanded = true; private categoryFilters = new Set(); + private allFiles: FileOrFolderElement[] = []; - constructor(private fileService: FileService, private baseFolderService: BaseFolderService, public dialog: MatDialog) { + constructor(private fileService: FileService, public dialog: MatDialog, private userRootComponent: UserRootComponent) { this.fileDataSource.filterPredicate = data => { return this.filterPredicate(data); } } ngOnInit(): void { - this.baseFolderService.findOrCreateBaseFolder().subscribe(baseFolderId => { - this.baseFolderId = baseFolderId; - this.refresh().subscribe(); - }); + let filesCache = this.userRootComponent.getFilesCache(); + this.baseFolderId = filesCache.baseFolder; + this.allFiles = filesCache.all; + this.populateFilesAndCategories(); + if (this.fileSort) { this.fileSort.sort(({id: 'name', start: 'asc'}) as MatSortable); this.fileDataSource.sort = this.fileSort; } + + this.checkForEmptyCategoriesToRemove(); } trashFile(element: FileElement) { this.fileService.trash(element.id) - .pipe(mergeMap(() => this.refresh())) - .subscribe(() => this.checkForEmptyCategoriesToRemove()); + .subscribe(() => { + this.refresh(); + }) } + // TODO: make private? refresh() { - return this.fileService.findAll() - .pipe(map(filesOrFolders => { - this.fileDataSource.data = filesOrFolders.filter(value => isFileElement(value)) - .map(value => value as FileElement); - this.categories.clear(); - filesOrFolders.filter(value => !isFileElement(value)) - .forEach(category => this.categories.set(category.id, category)); - - this.constructCategoryTree(); - })); + this.userRootComponent.refreshCacheAndReload(); } setCategory(element: FileElement) { @@ -114,28 +111,15 @@ export class FileListComponent implements OnInit { if (categories) { this.findOrCreateCategories(categories, this.baseFolderId) .pipe(mergeMap(categoryId => { - return this.fileService.setCategory(element.id, categoryId) - }), - mergeMap(() => this.refresh())) + return this.fileService.setCategory(element.id, categoryId) + })) .subscribe(_ => { - this.checkForEmptyCategoriesToRemove(); + this.refresh() }); } }) } - private checkForEmptyCategoriesToRemove() { - // Also remove empty categories which can happen when removing a category from a file - let removeCategoryRequests = this.removeEmptyCategories(); - if (removeCategoryRequests) { - zip(removeCategoryRequests) - // Do a refresh when all categories were removed - .pipe(mergeMap(() => this.refresh())) - .subscribe(() => { - }) - } - } - categoryHasChild = (_: number, node: FolderElement) => { return this.getChildren(node.id).length > 0; }; @@ -163,8 +147,7 @@ export class FileListComponent implements OnInit { getAncestorCategories(catId: string): FolderElement[] { let category = this.categories.get(catId); - // We should not include the base folder which is not to be considered as a category - if (category && category.id !== this.baseFolderId) { + if (category) { let categories = this.getAncestorCategories(category.parentId); categories.push(category); return categories; @@ -173,6 +156,30 @@ export class FileListComponent implements OnInit { } } + private populateFilesAndCategories() { + this.fileDataSource.data = this.allFiles.filter(value => isFileElement(value)) + .map(value => value as FileElement); + this.categories.clear(); + this.allFiles.filter(value => !isFileElement(value)) + // Filter out base folder which is not a category + .filter(value => value.id !== this.baseFolderId) + .forEach(category => this.categories.set(category.id, category)); + + this.constructCategoryTree(); + } + + private checkForEmptyCategoriesToRemove() { + // Also remove empty categories which can happen when removing a category from a file + let removeCategoryRequests = this.removeEmptyCategories(); + if (removeCategoryRequests) { + zip(removeCategoryRequests) + .subscribe(() => { + // Do a refresh when all categories were removed + this.refresh() + }) + } + } + /** * Filter by name (ignoring case) then filter by category */ diff --git a/src/app/file-list/file.service.ts b/src/app/file-list/file.service.ts index b96353f..204c86e 100644 --- a/src/app/file-list/file.service.ts +++ b/src/app/file-list/file.service.ts @@ -16,6 +16,7 @@ export class FileService { /** * Return all files managed by the app, except for the base folder */ + // TODO: replace calls with state param findAll(): Observable { const url = FileService.DRIVE_API_FILES_BASE_URL + '?q=' + encodeURI("trashed = false") + "&fields=" + encodeURI("files(id,name,createdTime,size,iconLink,webContentLink,mimeType,parents)"); return this.http.get(url).pipe(map(res => { diff --git a/src/app/homepage/homepage.component.html b/src/app/homepage/homepage.component.html index 4b5e758..c0884b7 100644 --- a/src/app/homepage/homepage.component.html +++ b/src/app/homepage/homepage.component.html @@ -1,7 +1,7 @@
- Setup rules + Setup rules   - +
diff --git a/src/app/homepage/homepage.component.spec.ts b/src/app/homepage/homepage.component.spec.ts index 25cf67d..1c3262f 100644 --- a/src/app/homepage/homepage.component.spec.ts +++ b/src/app/homepage/homepage.component.spec.ts @@ -4,7 +4,6 @@ import {AppModule} from "../app.module"; import {FileUploadComponent} from "../file-upload/file-upload.component"; import {FileListComponent} from "../file-list/file-list.component"; import {mock, when} from "strong-mock"; -import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {fakeAsync, tick} from "@angular/core/testing"; describe('HomepageComponent', () => { @@ -24,7 +23,8 @@ describe('HomepageComponent', () => { refresh: fileListComponent.refresh } }); - when(() => fileListComponent.refresh()).thenReturn(mustBeConsumedAsyncObservable(undefined)); + // TODO: simplify with direct page reload? + when(() => fileListComponent.refresh()).thenReturn(); MockRender(HomepageComponent); let fileUploadComponent = ngMocks.findInstance(FileUploadComponent); diff --git a/src/app/resolver/files.resolver.spec.ts b/src/app/resolver/files.resolver.spec.ts new file mode 100644 index 0000000..c044903 --- /dev/null +++ b/src/app/resolver/files.resolver.spec.ts @@ -0,0 +1,57 @@ +import {ActivatedRoute, RouterModule} from '@angular/router'; + +import {filesResolver} from './files.resolver'; +import {MockBuilder, NG_MOCKS_GUARDS, NG_MOCKS_RESOLVERS, ngMocks, Type} from "ng-mocks"; +import {RouterTestingModule} from "@angular/router/testing"; +import {AppModule} from "../app.module"; +import {fakeAsync} from "@angular/core/testing"; +import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; +import {when} from "strong-mock"; +import {mockFileService} from "../file-list/file.service.spec"; +import {of} from "rxjs"; +import {mockFileElement} from "../file-list/file-list.component.spec"; +import {UserRootComponent} from "../user-root/user-root.component"; +import {navigateTo} from "../../testing/common-testing-function.spec"; + +describe('filesResolver', () => { + beforeEach(() => { + return MockBuilder( + [ + RouterModule, + RouterTestingModule.withRoutes([]) + ], + AppModule, + ) + .exclude(NG_MOCKS_GUARDS) + .exclude(NG_MOCKS_RESOLVERS) + .keep(filesResolver); + }); + + + function getRouteData(component: Type) { + // Let's extract ActivatedRoute of the current component. + const el = ngMocks.find(component); + const route = ngMocks.findInstance(el, ActivatedRoute); + + return route.snapshot.data; + } + + it('should fetch baseFolder and files', fakeAsync(() => { + // Arrange + mockBaseFolderService(); + let fileService = mockFileService(); + let fileElement = mockFileElement('file1'); + when(() => fileService.findAll()).thenReturn(of([fileElement])) + + // Act + navigateTo('/'); + + // Assert + expect(getRouteData(UserRootComponent)).toEqual({ + files: { + baseFolder: 'baseFolderId', + all: [fileElement] + } + }); + })); +}); diff --git a/src/app/resolver/files.resolver.ts b/src/app/resolver/files.resolver.ts new file mode 100644 index 0000000..3f2150b --- /dev/null +++ b/src/app/resolver/files.resolver.ts @@ -0,0 +1,25 @@ +import {ResolveFn} from '@angular/router'; +import {inject} from "@angular/core"; +import {BaseFolderService} from "../file-upload/base-folder.service"; +import {map, Observable, zip} from "rxjs"; +import {FileService} from "../file-list/file.service"; +import {FileOrFolderElement} from "../file-list/file-list.component"; + +export interface FilesCache { + baseFolder: string, + all: FileOrFolderElement[] +} + +export const filesResolver: ResolveFn> = (route, state) => { + let baseFolderService = inject(BaseFolderService); + let fileService = inject(FileService); + return zip( + baseFolderService.findOrCreateBaseFolder(), + fileService.findAll() + ).pipe(map(([baseFolderId, allFiles]) => { + return { + baseFolder: baseFolderId, + all: allFiles + } + })) +}; diff --git a/src/app/route-strategy.service.ts b/src/app/route-strategy.service.ts new file mode 100644 index 0000000..c7b9082 --- /dev/null +++ b/src/app/route-strategy.service.ts @@ -0,0 +1,17 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, BaseRouteReuseStrategy, RouteReuseStrategy} from "@angular/router"; +import {UserRootComponent} from "./user-root/user-root.component"; + +@Injectable() +export class MyRouteReuseStrategy extends BaseRouteReuseStrategy { + + override shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { + if (future.component === UserRootComponent) { + // The UserRootComponent may have refreshed its data, so we should not reuse the route + return false; + } + return super.shouldReuseRoute(future, curr); + } +} + +export const routeReuseStrategyProvider = {provide: RouteReuseStrategy, useClass: MyRouteReuseStrategy} diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index c779a64..7784536 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -1,6 +1,6 @@

Setup rules

diff --git a/src/app/user-root/user-root.component.html b/src/app/user-root/user-root.component.html new file mode 100644 index 0000000..0680b43 --- /dev/null +++ b/src/app/user-root/user-root.component.html @@ -0,0 +1 @@ + diff --git a/src/app/user-root/user-root.component.scss b/src/app/user-root/user-root.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/user-root/user-root.component.spec.ts b/src/app/user-root/user-root.component.spec.ts new file mode 100644 index 0000000..72e7f1d --- /dev/null +++ b/src/app/user-root/user-root.component.spec.ts @@ -0,0 +1,12 @@ +import {UserRootComponent} from './user-root.component'; +import {MockBuilder, MockRender} from "ng-mocks"; +import {AppModule} from "../app.module"; + +describe('UserRootComponent', () => { + beforeEach(() => MockBuilder(UserRootComponent, AppModule)); + + it('should create', () => { + let component = MockRender(UserRootComponent).point.componentInstance; + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/user-root/user-root.component.ts b/src/app/user-root/user-root.component.ts new file mode 100644 index 0000000..f9723de --- /dev/null +++ b/src/app/user-root/user-root.component.ts @@ -0,0 +1,34 @@ +import {Component} from '@angular/core'; +import {ActivatedRoute, Router} from "@angular/router"; +import {FilesCache} from "../resolver/files.resolver"; + +@Component({ + selector: 'app-user-root', + templateUrl: './user-root.component.html', + styleUrls: ['./user-root.component.scss'] +}) +export class UserRootComponent { + static reloadRouteData = false; + + constructor(private activatedRoute: ActivatedRoute, private router: Router) { + } + + static shouldReloadRouteData() { + if (UserRootComponent.reloadRouteData) { + UserRootComponent.reloadRouteData = false; + return true; + } + return false; + } + + getFilesCache(): FilesCache { + return this.activatedRoute.snapshot.data["files"]; + } + + refreshCacheAndReload() { + UserRootComponent.reloadRouteData = true; + this.router.navigate([this.router.url], { + onSameUrlNavigation: "reload" + }) + } +} diff --git a/src/testing/common-testing-function.spec.ts b/src/testing/common-testing-function.spec.ts index 8284743..5e3b2fd 100644 --- a/src/testing/common-testing-function.spec.ts +++ b/src/testing/common-testing-function.spec.ts @@ -1,5 +1,9 @@ import {Observable, Subscriber, TeardownLogic} from "rxjs"; import {db} from "../app/database/db"; +import {MockRender, ngMocks} from "ng-mocks"; +import {Router, RouterOutlet} from "@angular/router"; +import {Location} from "@angular/common"; +import {tick} from "@angular/core/testing"; export async function findAsyncSequential( array: T[], @@ -68,3 +72,21 @@ export async function dbCleanUp() { db.createSchema(); await db.open(); } + +export function navigateTo(path: string) { + const fixture = MockRender(RouterOutlet, {}); + const router = ngMocks.get(Router); + const location = ngMocks.get(Location); + + // Let's switch to the route with the resolver. + location.go(path); + + if (fixture.ngZone) { + fixture.ngZone.run(() => router.initialNavigation()); + tick(); // is needed for rendering of the current route. + fixture.detectChanges(); + } + + // Checking that we are on the right page. + expect(location.path()).toEqual(path); +} From 51b9ecb68592fd3ab46d47da57452476786e3d8a Mon Sep 17 00:00:00 2001 From: Musholic Date: Tue, 9 Jan 2024 15:27:41 +0100 Subject: [PATCH 27/66] [us40] After upload, refresh page with new refresh method --- src/app/file-list/file-list.component.ts | 9 +++--- .../file-upload/file-upload.component.spec.ts | 24 ++++++++++----- src/app/file-upload/file-upload.component.ts | 11 ++++--- src/app/homepage/homepage.component.html | 4 +-- src/app/homepage/homepage.component.spec.ts | 29 +------------------ 5 files changed, 28 insertions(+), 49 deletions(-) diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index 3ce0ffb..759e460 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -91,11 +91,6 @@ export class FileListComponent implements OnInit { }) } - // TODO: make private? - refresh() { - this.userRootComponent.refreshCacheAndReload(); - } - setCategory(element: FileElement) { let data: SelectFileCategoryDialogData = { file: element, @@ -156,6 +151,10 @@ export class FileListComponent implements OnInit { } } + private refresh() { + this.userRootComponent.refreshCacheAndReload(); + } + private populateFilesAndCategories() { this.fileDataSource.data = this.allFiles.filter(value => isFileElement(value)) .map(value => value as FileElement); diff --git a/src/app/file-upload/file-upload.component.spec.ts b/src/app/file-upload/file-upload.component.spec.ts index 023b6c9..87f2c15 100644 --- a/src/app/file-upload/file-upload.component.spec.ts +++ b/src/app/file-upload/file-upload.component.spec.ts @@ -14,12 +14,18 @@ import {MatButtonHarness} from "@angular/material/button/testing"; import {GooglePickerService} from "./google-picker.service"; import {mockFileUploadService} from "./file-upload.service.spec"; import {BreakpointObserver} from "@angular/cdk/layout"; +import {UserRootComponent} from "../user-root/user-root.component"; describe('FileUploadComponent', () => { beforeEach(() => { return MockBuilder(FileUploadComponent, AppModule) .keep(MatIconModule) .keep(BreakpointObserver) + // For some reason, we need to explicitly add a provider for UserRootComponent + .provide({ + provide: UserRootComponent, + useValue: mock() + }) }); it('should create', () => { @@ -81,17 +87,18 @@ describe('FileUploadComponent', () => { type: HttpEventType.Response } as HttpResponse)) + let userRootComponent = ngMocks.get(UserRootComponent); + // A page refresh is expected + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + const fixture = MockRender(FileUploadComponent); const page = new Page(fixture); - let component = fixture.point.componentInstance; - let finishedEventReceived = false; - component.onRefreshRequest.subscribe(() => finishedEventReceived = true) // Act page.uploadFile(file); // Assert - expect(finishedEventReceived).toBeTruthy(); + // No failure from mock setup }) }) @@ -102,17 +109,18 @@ describe('FileUploadComponent', () => { // The user has picked a file when we show the picker when(() => showMock()).thenResolve(undefined); + let userRootComponent = ngMocks.get(UserRootComponent); + // A page refresh is expected + when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + const fixture = MockRender(FileUploadComponent); const page = new Page(fixture); - let component = fixture.point.componentInstance; - let finishedEventReceived = false; - component.onRefreshRequest.subscribe(() => finishedEventReceived = true) // Act await page.openGooglePicker(); // Assert - expect(finishedEventReceived).toBeTruthy(); + // No failure from mock setup }); }); }); diff --git a/src/app/file-upload/file-upload.component.ts b/src/app/file-upload/file-upload.component.ts index e1b34b5..a964ca2 100644 --- a/src/app/file-upload/file-upload.component.ts +++ b/src/app/file-upload/file-upload.component.ts @@ -1,8 +1,9 @@ -import {Component, EventEmitter, Output} from '@angular/core'; +import {Component} from '@angular/core'; import {FileUploadService, toFileOrBlob} from "./file-upload.service"; import {FileUploadProgress} from "./file-upload-element/file-upload-element.component"; import {HttpEventType} from "@angular/common/http"; import {GooglePickerService} from "./google-picker.service"; +import {UserRootComponent} from "../user-root/user-root.component"; @Component({ selector: 'app-file-upload', @@ -13,9 +14,7 @@ export class FileUploadComponent { files: FileUploadProgress[] = [] - @Output() onRefreshRequest = new EventEmitter() - - constructor(private fileUploadService: FileUploadService, private googlePickerService: GooglePickerService) { + constructor(private fileUploadService: FileUploadService, private googlePickerService: GooglePickerService, private userRootComponent: UserRootComponent) { } onFileSelected(event: Event) { @@ -31,7 +30,7 @@ export class FileUploadComponent { async showGooglePicker() { this.googlePickerService.show() - .then(_ => this.onRefreshRequest.emit()); + .then(() => this.userRootComponent.refreshCacheAndReload()); } private upload(file: File) { @@ -40,7 +39,7 @@ export class FileUploadComponent { this.fileUploadService.upload(toFileOrBlob(file)) .subscribe(e => { if (e.type === HttpEventType.Response) { - this.onRefreshRequest.emit(); + this.userRootComponent.refreshCacheAndReload(); } else { fileProgress.loaded = e.loaded; if (e.total != null) { diff --git a/src/app/homepage/homepage.component.html b/src/app/homepage/homepage.component.html index c0884b7..53bf0db 100644 --- a/src/app/homepage/homepage.component.html +++ b/src/app/homepage/homepage.component.html @@ -2,6 +2,6 @@
Setup rules   - +
- + diff --git a/src/app/homepage/homepage.component.spec.ts b/src/app/homepage/homepage.component.spec.ts index 1c3262f..4ff118a 100644 --- a/src/app/homepage/homepage.component.spec.ts +++ b/src/app/homepage/homepage.component.spec.ts @@ -1,10 +1,6 @@ import {HomepageComponent} from './homepage.component'; -import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; +import {MockBuilder, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; -import {FileUploadComponent} from "../file-upload/file-upload.component"; -import {FileListComponent} from "../file-list/file-list.component"; -import {mock, when} from "strong-mock"; -import {fakeAsync, tick} from "@angular/core/testing"; describe('HomepageComponent', () => { @@ -14,27 +10,4 @@ describe('HomepageComponent', () => { let component = MockRender(HomepageComponent).point.componentInstance; expect(component).toBeTruthy(); }); - - it('when upload is asking for a refresh, it refresh the file list', fakeAsync(() => { - // Arrange - let fileListComponent = mock(); - MockInstance(FileListComponent, () => { - return { - refresh: fileListComponent.refresh - } - }); - // TODO: simplify with direct page reload? - when(() => fileListComponent.refresh()).thenReturn(); - - MockRender(HomepageComponent); - let fileUploadComponent = ngMocks.findInstance(FileUploadComponent); - - // Act - fileUploadComponent.onRefreshRequest.emit(); - - // Assert - // No failure in mock setup - tick(); - - })); }); From dcca98fdc88edad312f284ffde6cdc7475b2dd56 Mon Sep 17 00:00:00 2001 From: Musholic Date: Wed, 10 Jan 2024 17:02:03 +0100 Subject: [PATCH 28/66] [us40] RuleService: use cache for fileService.findAll --- src/app/file-list/file-list.component.spec.ts | 18 ++-------- .../file-upload/file-upload.component.spec.ts | 12 +++---- src/app/rules/rule.service.spec.ts | 22 ++++++++---- src/app/rules/rule.service.ts | 34 +++++++++---------- src/app/rules/rules.component.spec.ts | 2 ++ src/app/rules/rules.component.ts | 3 +- src/app/user-root/user-root.component.spec.ts | 16 +++++++-- 7 files changed, 58 insertions(+), 49 deletions(-) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index eb4c76b..da1790a 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -35,8 +35,8 @@ import {MatChipGridHarness} from "@angular/material/chips/testing"; import {mockFileService} from "./file.service.spec"; import {MatSortModule} from "@angular/material/sort"; import {BreakpointObserver} from "@angular/cdk/layout"; -import {FilesCache} from "../resolver/files.resolver"; import {UserRootComponent} from "../user-root/user-root.component"; +import {mockFilesCache} from "../user-root/user-root.component.spec"; function mockRenderAndWaitForChanges() { let fixture = MockRender(FileListComponent, null, {reset: true}); @@ -48,17 +48,6 @@ function mockRenderAndWaitForChanges() { return fixture; } -function mockFilesCacheInRouteData() { - let filesCache: FilesCache = { - baseFolder: 'baseFolderId', - all: [] - }; - let userRootComponent = ngMocks.findInstance(UserRootComponent); - when(() => userRootComponent.getFilesCache()).thenReturn(filesCache); - - return filesCache; -} - describe('FileListComponent', () => { beforeEach(() => MockBuilder(FileListComponent, AppModule) .mock(UserRootComponent) @@ -82,7 +71,7 @@ describe('FileListComponent', () => { it('should create (no element)', fakeAsync(() => { // Arrange - mockFilesCacheInRouteData(); + mockFilesCache([]); // Act const component = mockRenderAndWaitForChanges().point.componentInstance; @@ -748,7 +737,6 @@ function mockFolderElement(name: string, parentId: string = 'baseFolderId'): Fol } function mockListItemsAndCategories(itemsAndCategories: (FileElement | FolderElement)[], fillEachCategory: boolean = false) { - let filesCache = mockFilesCacheInRouteData(); if (fillEachCategory) { let categories = itemsAndCategories.filter(file => !isFileElement(file)) .map(value => value as FolderElement); @@ -756,7 +744,7 @@ function mockListItemsAndCategories(itemsAndCategories: (FileElement | FolderEle itemsAndCategories.push(mockFileElement(cat.name + "_file", cat.id)) }) } - filesCache.all = itemsAndCategories; + mockFilesCache(itemsAndCategories); return mockFileService(); } diff --git a/src/app/file-upload/file-upload.component.spec.ts b/src/app/file-upload/file-upload.component.spec.ts index 87f2c15..e92a497 100644 --- a/src/app/file-upload/file-upload.component.spec.ts +++ b/src/app/file-upload/file-upload.component.spec.ts @@ -87,13 +87,13 @@ describe('FileUploadComponent', () => { type: HttpEventType.Response } as HttpResponse)) + const fixture = MockRender(FileUploadComponent); + const page = new Page(fixture); + let userRootComponent = ngMocks.get(UserRootComponent); // A page refresh is expected when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); - const fixture = MockRender(FileUploadComponent); - const page = new Page(fixture); - // Act page.uploadFile(file); @@ -109,13 +109,13 @@ describe('FileUploadComponent', () => { // The user has picked a file when we show the picker when(() => showMock()).thenResolve(undefined); + const fixture = MockRender(FileUploadComponent); + const page = new Page(fixture); + let userRootComponent = ngMocks.get(UserRootComponent); // A page refresh is expected when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); - const fixture = MockRender(FileUploadComponent); - const page = new Page(fixture); - // Act await page.openGooglePicker(); diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 70a28d8..a4160af 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -1,3 +1,4 @@ +import {UserRootComponent} from "../user-root/user-root.component"; import {RuleService} from './rule.service'; import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; @@ -10,6 +11,7 @@ import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; import {FileService} from "../file-list/file.service"; import {mockRuleRepository} from "./rule.repository.spec"; import {getSampleRules} from "./rules.component.spec"; +import {mockFilesCache} from "../user-root/user-root.component.spec"; function mockBillCategoryFindOrCreate(fileService: FileService) { @@ -21,7 +23,13 @@ function mockBillCategoryFindOrCreate(fileService: FileService) { } describe('RuleService', () => { - beforeEach(() => MockBuilder(RuleService, AppModule)); + beforeEach(() => MockBuilder(RuleService, AppModule) + // For some reason, we need to explicitly add a provider for UserRootComponent + .provide({ + provide: UserRootComponent, + useValue: mock() + }) + ); it('should be created', () => { // Act @@ -43,15 +51,15 @@ describe('RuleService', () => { when(() => ruleRepository.findAll()) .thenResolve(getSampleRules()); - let file = mockFileElement('electricity_bill.pdf'); - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([file])) - // The file should be set to the bills category + let file = mockFileElement('electricity_bill.pdf'); when(() => fileService.setCategory(file.id, 'billsCatId489')) .thenReturn(mustBeConsumedAsyncObservable(undefined)); const service = MockRender(RuleService).point.componentInstance; + mockFilesCache([file]); + // Act service.runAll().subscribe(); @@ -68,15 +76,15 @@ describe('RuleService', () => { mockBillCategoryFindOrCreate(fileService); - let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([file])) - let ruleRepository = mockRuleRepository(); when(() => ruleRepository.findAll()) .thenResolve(getSampleRules()); const service = MockRender(RuleService).point.componentInstance; + let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); + mockFilesCache([file]); + // Act service.runAll().subscribe(); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 9cecb07..f041690 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -4,29 +4,27 @@ import {filter, from, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {BaseFolderService} from "../file-upload/base-folder.service"; import {Rule, RuleRepository} from "./rule.repository"; +import {UserRootComponent} from "../user-root/user-root.component"; -@Injectable({ - providedIn: 'root' -}) +@Injectable() export class RuleService { - constructor(private fileService: FileService, private baseFolderService: BaseFolderService, private ruleRepository: RuleRepository) { + constructor(private fileService: FileService, private baseFolderService: BaseFolderService, + private ruleRepository: RuleRepository, private userRootComponent: UserRootComponent) { } runAll(): Observable { return from(this.ruleRepository.findAll()) .pipe(mergeMap(rules => { - return this.fileService.findAll() - .pipe(mergeMap(fileOrFolders => { - // Get all files - let files = fileOrFolders.filter(file => isFileElement(file)) - .map(value => value as FileElement); + let fileOrFolders = this.userRootComponent.getFilesCache().all; + // Get all files + let files = fileOrFolders.filter(file => isFileElement(file)) + .map(value => value as FileElement); - // Run the script for each file to get the associated category - let fileToCategoryMap = this.computeFileToCategoryMap(files, rules); + // Run the script for each file to get the associated category + let fileToCategoryMap = this.computeFileToCategoryMap(files, rules); - // Set the computed category for each files - return this.setAllFileCategory(fileToCategoryMap); - })) + // Set the computed category for each files + return this.setAllFileCategory(fileToCategoryMap); })); } @@ -38,6 +36,10 @@ export class RuleService { return this.ruleRepository.findAll(); } + delete(rule: Rule) { + return this.ruleRepository.delete(rule); + } + /** * Run the given rules on the given files and return the associated category for each file that got a matching rule */ @@ -95,8 +97,4 @@ export class RuleService { } return of(categoryId); } - - delete(rule: Rule) { - return this.ruleRepository.delete(rule); - } } diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 9dfef03..8ebee0e 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -18,6 +18,7 @@ import {MatChipsModule} from "@angular/material/chips"; import {MatChipGridHarness} from "@angular/material/chips/testing"; import {Rule} from "./rule.repository"; import {BreakpointObserver} from "@angular/cdk/layout"; +import {RuleService} from "./rule.service"; describe('RulesComponent', () => { beforeEach(() => MockBuilder(RulesComponent, AppModule) @@ -26,6 +27,7 @@ describe('RulesComponent', () => { .keep(FormsModule) .keep(MatChipsModule) .keep(BreakpointObserver) + .mock(RuleService) .replace(BrowserAnimationsModule, NoopAnimationsModule) ); diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index c9de5f9..08c170a 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -8,7 +8,8 @@ import {Rule} from "./rule.repository"; @Component({ selector: 'app-rules', templateUrl: './rules.component.html', - styleUrls: ['./rules.component.scss'] + styleUrls: ['./rules.component.scss'], + providers: [RuleService] }) export class RulesComponent { readonly separatorKeysCodes = [ENTER] as const; diff --git a/src/app/user-root/user-root.component.spec.ts b/src/app/user-root/user-root.component.spec.ts index 72e7f1d..7cbec9c 100644 --- a/src/app/user-root/user-root.component.spec.ts +++ b/src/app/user-root/user-root.component.spec.ts @@ -1,12 +1,24 @@ import {UserRootComponent} from './user-root.component'; -import {MockBuilder, MockRender} from "ng-mocks"; +import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; +import {FileOrFolderElement} from "../file-list/file-list.component"; +import {FilesCache} from "../resolver/files.resolver"; +import {when} from "strong-mock"; describe('UserRootComponent', () => { - beforeEach(() => MockBuilder(UserRootComponent, AppModule)); + beforeEach(() => MockBuilder(UserRootComponent, AppModule)) it('should create', () => { let component = MockRender(UserRootComponent).point.componentInstance; expect(component).toBeTruthy(); }); }); + +export function mockFilesCache(files: FileOrFolderElement[]) { + let filesCache: FilesCache = { + baseFolder: 'baseFolderId', + all: files + }; + let userRootComponent = ngMocks.findInstance(UserRootComponent); + when(() => userRootComponent.getFilesCache()).thenReturn(filesCache); +} From f68ef251498385f0b4e2c938cbd8cfcf1dec9623 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 11 Jan 2024 10:36:19 +0100 Subject: [PATCH 29/66] [us40] DatabaseBackupAndRestoreService: use cache for fileService.findAll --- ...atabase-backup-and-restore.service.spec.ts | 27 +++---- .../database-backup-and-restore.service.ts | 77 +++++++++---------- src/app/file-list/file.service.ts | 1 - src/app/rules/rule.repository.ts | 4 +- src/app/rules/rule.service.spec.ts | 24 +++--- src/app/rules/rules.component.ts | 14 ++-- src/app/user-root/user-root.component.ts | 5 +- 7 files changed, 78 insertions(+), 74 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index e092031..446a3c4 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -5,7 +5,6 @@ import {mockFileUploadService} from "../file-upload/file-upload.service.spec"; import {It, mock, when} from "strong-mock"; import {dbCleanUp, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {HttpClientModule, HttpEventType, HttpResponse} from "@angular/common/http"; -import {mockFileService} from "../file-list/file.service.spec"; import {mockFileElement} from "../file-list/file-list.component.spec"; import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; import {fakeAsync, TestBed, tick} from "@angular/core/testing"; @@ -13,10 +12,16 @@ import {db} from "./db"; import {BehaviorSubject, lastValueFrom} from "rxjs"; import {mockBackgroundTaskService} from "../background-task/background-task.service.spec"; import {Progress} from "../background-task/background-task.service"; +import {UserRootComponent} from "../user-root/user-root.component"; +import {mockFilesCache} from "../user-root/user-root.component.spec"; describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) .replace(HttpClientModule, HttpClientTestingModule) + .provide({ + provide: UserRootComponent, + useValue: mock() + }) ); // Db cleanup after each test @@ -35,21 +40,19 @@ describe('DatabaseBackupAndRestoreService', () => { describe('restore', () => { it('The database should be automatically restored', fakeAsync(async () => { // Arrange - let fileService = mockFileService(); - let dbBackupFile = mockFileElement('db.backup'); - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([dbBackupFile])); - let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); when(() => backgroundTaskService.showProgress("Automatic restore", "Downloading last backup", 2)) .thenReturn(progress); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); - when(() => progress.next({index: 2, description: "Importing backup", value: 50})).thenReturn(); when(() => progress.next({index: 2, value: 100})).thenReturn(); - let fixture = MockRender(DatabaseBackupAndRestoreService); let databaseBackupAndRestoreService = fixture.point.componentInstance; + let dbBackupFile = mockFileElement('db.backup'); + mockFilesCache([dbBackupFile]); + // Act let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore()); @@ -99,9 +102,6 @@ describe('DatabaseBackupAndRestoreService', () => { describe('backup', () => { it('Should upload a new backup file when there is no backup yet', async () => { // Arrange - let fileService = mockFileService(); - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([])); - let fileUploadService = mockFileUploadService(); when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}))) .thenReturn(mustBeConsumedAsyncObservable({ @@ -117,6 +117,8 @@ describe('DatabaseBackupAndRestoreService', () => { const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + mockFilesCache([]); + // Act let backupPromise = lastValueFrom(databaseBackupAndRestoreService.backup()); @@ -127,10 +129,7 @@ describe('DatabaseBackupAndRestoreService', () => { it('should overwrite the existing backup file when there is already an existing backup', async () => { // Arrange - let fileService = mockFileService(); let dbBackupFile = mockFileElement('db.backup'); - when(() => fileService.findAll()).thenReturn(mustBeConsumedAsyncObservable([dbBackupFile])); - let fileUploadService = mockFileUploadService(); when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}), dbBackupFile.id)) .thenReturn(mustBeConsumedAsyncObservable({ @@ -146,6 +145,8 @@ describe('DatabaseBackupAndRestoreService', () => { const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + mockFilesCache([dbBackupFile]); + // Act let backupPromise = lastValueFrom(databaseBackupAndRestoreService.backup()); diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 06cd72a..3c51ca3 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -7,16 +7,17 @@ import {FileService} from "../file-list/file.service"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {HttpClient, HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; import {BackgroundTaskService} from "../background-task/background-task.service"; +import {UserRootComponent} from "../user-root/user-root.component"; -@Injectable({ - providedIn: 'root' -}) +@Injectable() export class DatabaseBackupAndRestoreService { private static readonly DB_NAME = 'db.backup'; - constructor(private fileUploadService: FileUploadService, private fileService: FileService, private http: HttpClient, - private backgroundTaskService: BackgroundTaskService) { + constructor(private fileUploadService: FileUploadService, private http: HttpClient, + private backgroundTaskService: BackgroundTaskService, private userRootComponent: UserRootComponent) { + // this.restore().subscribe(); + // TODO: check refresh after restore } backup() { @@ -25,10 +26,8 @@ export class DatabaseBackupAndRestoreService { return from(exportDB(db)) .pipe(tap(() => progress.next({index: 2, value: 50, description: "Uploading backup"})), mergeMap(blob => { - return this.findExistingDbFile() - .pipe(mergeMap(dbFile => { - return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); - })); + let dbFile = this.findExistingDbFile(); + return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); }), tap(httpEvent => this.backgroundTaskService.updateProgress(progress, httpEvent))); } @@ -36,39 +35,37 @@ export class DatabaseBackupAndRestoreService { restore(): Observable { let progress = this.backgroundTaskService.showProgress('Automatic restore', "Downloading last backup", 2); - return this.findExistingDbFile().pipe( - tap(() => progress.next({index: 2, value: 50, description: 'Importing backup'})), - mergeMap(dbFile => { - if (dbFile) { - let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + dbFile.id + '?alt=media'; - return this.http.get(dlLink, {responseType: "blob", observe: "events", reportProgress: true}); - } - return of(); - }), - filter((e: HttpEvent): e is HttpProgressEvent | HttpResponse => e.type === HttpEventType.DownloadProgress || e.type === HttpEventType.Response), - tap(event => this.backgroundTaskService.updateProgress(progress, event)), - last(), - mergeMap(event => { - if (event.type === HttpEventType.Response && event.body) { - return of(event.body); - } else { - return of(); - } - }), - mergeMap(dbDownloadResponse => { - return from(importDB(dbDownloadResponse)); - }), - tap(() => progress.next({index: 2, value: 100})), - map(() => void 0)); + let dbFile = this.findExistingDbFile(); + if (dbFile) { + let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + dbFile.id + '?alt=media'; + return this.http.get(dlLink, {responseType: "blob", observe: "events", reportProgress: true}) + .pipe( + filter((e: HttpEvent): e is HttpProgressEvent | HttpResponse => + e.type === HttpEventType.DownloadProgress || e.type === HttpEventType.Response), + tap(event => this.backgroundTaskService.updateProgress(progress, event)), + last(), + mergeMap(event => { + if (event.type === HttpEventType.Response && event.body) { + return of(event.body); + } else { + return of(); + } + }), + mergeMap(dbDownloadResponse => { + return from(importDB(dbDownloadResponse)); + }), + tap(() => progress.next({index: 2, value: 100})), + map(() => void 0)); + } else { + return of(); + } } private findExistingDbFile() { - return this.fileService.findAll() - .pipe(map(files => { - // TODO: ensure there cannot be any conflicts with user files - return files.filter(f => isFileElement(f)) - .map(f => f as FileElement) - .find(file => file.name === DatabaseBackupAndRestoreService.DB_NAME) - })); + let files = this.userRootComponent.getFilesCache().all; + // TODO: ensure there cannot be any conflicts with user files + return files.filter(f => isFileElement(f)) + .map(f => f as FileElement) + .find(file => file.name === DatabaseBackupAndRestoreService.DB_NAME) } } diff --git a/src/app/file-list/file.service.ts b/src/app/file-list/file.service.ts index 204c86e..b96353f 100644 --- a/src/app/file-list/file.service.ts +++ b/src/app/file-list/file.service.ts @@ -16,7 +16,6 @@ export class FileService { /** * Return all files managed by the app, except for the base folder */ - // TODO: replace calls with state param findAll(): Observable { const url = FileService.DRIVE_API_FILES_BASE_URL + '?q=' + encodeURI("trashed = false") + "&fields=" + encodeURI("files(id,name,createdTime,size,iconLink,webContentLink,mimeType,parents)"); return this.http.get(url).pipe(map(res => { diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 24ed1a0..fca6279 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -9,9 +9,7 @@ export interface Rule { script: string; } -@Injectable({ - providedIn: 'root' -}) +@Injectable() export class RuleRepository { constructor(private databaseBackupAndRestoreService: DatabaseBackupAndRestoreService) { diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index a4160af..6346b0e 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -1,6 +1,6 @@ import {UserRootComponent} from "../user-root/user-root.component"; import {RuleService} from './rule.service'; -import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; import {mockFileService} from "../file-list/file.service.spec"; import {mock, when} from "strong-mock"; @@ -9,9 +9,9 @@ import {fakeAsync, tick} from "@angular/core/testing"; import {mockFileElement} from "../file-list/file-list.component.spec"; import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; import {FileService} from "../file-list/file.service"; -import {mockRuleRepository} from "./rule.repository.spec"; import {getSampleRules} from "./rules.component.spec"; import {mockFilesCache} from "../user-root/user-root.component.spec"; +import {RuleRepository} from "./rule.repository"; function mockBillCategoryFindOrCreate(fileService: FileService) { @@ -24,6 +24,10 @@ function mockBillCategoryFindOrCreate(fileService: FileService) { describe('RuleService', () => { beforeEach(() => MockBuilder(RuleService, AppModule) + .provide({ + provide: RuleRepository, + useValue: mock() + }) // For some reason, we need to explicitly add a provider for UserRootComponent .provide({ provide: UserRootComponent, @@ -47,10 +51,6 @@ describe('RuleService', () => { let fileService = mockFileService(); mockBillCategoryFindOrCreate(fileService); - let ruleRepository = mockRuleRepository(); - when(() => ruleRepository.findAll()) - .thenResolve(getSampleRules()); - // The file should be set to the bills category let file = mockFileElement('electricity_bill.pdf'); when(() => fileService.setCategory(file.id, 'billsCatId489')) @@ -58,6 +58,11 @@ describe('RuleService', () => { const service = MockRender(RuleService).point.componentInstance; + let ruleRepository = ngMocks.findInstance(RuleRepository); + when(() => ruleRepository.findAll()) + .thenResolve(getSampleRules()); + + mockFilesCache([file]); // Act @@ -76,12 +81,13 @@ describe('RuleService', () => { mockBillCategoryFindOrCreate(fileService); - let ruleRepository = mockRuleRepository(); - when(() => ruleRepository.findAll()) - .thenResolve(getSampleRules()); const service = MockRender(RuleService).point.componentInstance; + let ruleRepository = ngMocks.findInstance(RuleRepository); + when(() => ruleRepository.findAll()) + .thenResolve(getSampleRules()); + let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); mockFilesCache([file]); diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 08c170a..a0cf7d5 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -48,17 +48,17 @@ export class RulesComponent { event.chipInput.clear(); } - private refresh() { - this.ruleService.findAll() - .then(rules => { - this.rules = rules; - }) - } - delete(rule: Rule) { this.ruleService.delete(rule) .then(() => { this.refresh(); }) } + + private refresh() { + this.ruleService.findAll() + .then(rules => { + this.rules = rules; + }) + } } diff --git a/src/app/user-root/user-root.component.ts b/src/app/user-root/user-root.component.ts index f9723de..e35a42f 100644 --- a/src/app/user-root/user-root.component.ts +++ b/src/app/user-root/user-root.component.ts @@ -1,11 +1,14 @@ import {Component} from '@angular/core'; import {ActivatedRoute, Router} from "@angular/router"; import {FilesCache} from "../resolver/files.resolver"; +import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; +import {RuleRepository} from "../rules/rule.repository"; @Component({ selector: 'app-user-root', templateUrl: './user-root.component.html', - styleUrls: ['./user-root.component.scss'] + styleUrls: ['./user-root.component.scss'], + providers: [RuleRepository, DatabaseBackupAndRestoreService] }) export class UserRootComponent { static reloadRouteData = false; From e24e57df2bc7b95ded25ccfbb4a0f0f3918ba95c Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 11 Jan 2024 14:20:40 +0100 Subject: [PATCH 30/66] [us40] FileUploadService: use cache instead of baseFolderService.findOrCreateBaseFolder --- src/app/app-routing.module.ts | 5 +- ...atabase-backup-and-restore.service.spec.ts | 58 +++++++++---------- .../database-backup-and-restore.service.ts | 6 +- src/app/file-list/file-list.component.spec.ts | 47 ++++++++------- src/app/file-list/file-list.component.ts | 11 ++-- .../file-upload/file-upload.component.spec.ts | 46 ++++++++------- src/app/file-upload/file-upload.component.ts | 8 +-- .../file-upload/file-upload.service.spec.ts | 24 ++++---- src/app/file-upload/file-upload.service.ts | 32 +++++----- .../files-cache/files-cache.service.spec.ts | 31 ++++++++++ src/app/files-cache/files-cache.service.ts | 38 ++++++++++++ .../files.resolver.spec.ts | 0 .../files.resolver.ts | 0 src/app/rules/rule.repository.spec.ts | 22 ++++--- src/app/rules/rule.service.spec.ts | 17 +++--- src/app/rules/rule.service.ts | 6 +- src/app/user-root/user-root.component.spec.ts | 14 +---- src/app/user-root/user-root.component.ts | 29 ++-------- 18 files changed, 210 insertions(+), 184 deletions(-) create mode 100644 src/app/files-cache/files-cache.service.spec.ts create mode 100644 src/app/files-cache/files-cache.service.ts rename src/app/{resolver => files-cache}/files.resolver.spec.ts (100%) rename src/app/{resolver => files-cache}/files.resolver.ts (100%) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 042dde5..799a5fe 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,8 +4,9 @@ import {HomepageComponent} from "./homepage/homepage.component"; import {authGuard} from "./auth/auth.guard"; import {LoginComponent} from "./login/login.component"; import {RulesComponent} from "./rules/rules.component"; -import {filesResolver} from "./resolver/files.resolver"; +import {filesResolver} from "./files-cache/files.resolver"; import {UserRootComponent} from "./user-root/user-root.component"; +import {FilesCacheService} from "./files-cache/files-cache.service"; const routes: Routes = [ { @@ -14,7 +15,7 @@ const routes: Routes = [ canActivate: [authGuard], resolve: {files: filesResolver}, runGuardsAndResolvers: () => { - return UserRootComponent.shouldReloadRouteData(); + return FilesCacheService.shouldReloadRouteData(); }, children: [ { diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 446a3c4..818f97c 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -1,7 +1,6 @@ import {DatabaseBackupAndRestoreService} from './database-backup-and-restore.service'; -import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; +import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; -import {mockFileUploadService} from "../file-upload/file-upload.service.spec"; import {It, mock, when} from "strong-mock"; import {dbCleanUp, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {HttpClientModule, HttpEventType, HttpResponse} from "@angular/common/http"; @@ -12,15 +11,20 @@ import {db} from "./db"; import {BehaviorSubject, lastValueFrom} from "rxjs"; import {mockBackgroundTaskService} from "../background-task/background-task.service.spec"; import {Progress} from "../background-task/background-task.service"; -import {UserRootComponent} from "../user-root/user-root.component"; -import {mockFilesCache} from "../user-root/user-root.component.spec"; +import {mockFilesCacheService} from "../files-cache/files-cache.service.spec"; +import {FileUploadService} from "../file-upload/file-upload.service"; +import {FilesCacheService} from "../files-cache/files-cache.service"; describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) .replace(HttpClientModule, HttpClientTestingModule) .provide({ - provide: UserRootComponent, - useValue: mock() + provide: FileUploadService, + useValue: mock() + }) + .provide({ + provide: FilesCacheService, + useValue: mock() }) ); @@ -51,7 +55,7 @@ describe('DatabaseBackupAndRestoreService', () => { let databaseBackupAndRestoreService = fixture.point.componentInstance; let dbBackupFile = mockFileElement('db.backup'); - mockFilesCache([dbBackupFile]); + mockFilesCacheService([dbBackupFile]); // Act let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore()); @@ -102,12 +106,6 @@ describe('DatabaseBackupAndRestoreService', () => { describe('backup', () => { it('Should upload a new backup file when there is no backup yet', async () => { // Arrange - let fileUploadService = mockFileUploadService(); - when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}))) - .thenReturn(mustBeConsumedAsyncObservable({ - type: HttpEventType.Response - } as HttpResponse)); - let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) @@ -117,7 +115,13 @@ describe('DatabaseBackupAndRestoreService', () => { const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; - mockFilesCache([]); + mockFilesCacheService([]); + + let fileUploadService = ngMocks.get(FileUploadService); + when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}))) + .thenReturn(mustBeConsumedAsyncObservable({ + type: HttpEventType.Response + } as HttpResponse)); // Act let backupPromise = lastValueFrom(databaseBackupAndRestoreService.backup()); @@ -129,13 +133,6 @@ describe('DatabaseBackupAndRestoreService', () => { it('should overwrite the existing backup file when there is already an existing backup', async () => { // Arrange - let dbBackupFile = mockFileElement('db.backup'); - let fileUploadService = mockFileUploadService(); - when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}), dbBackupFile.id)) - .thenReturn(mustBeConsumedAsyncObservable({ - type: HttpEventType.Response - } as HttpResponse)); - let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) @@ -145,7 +142,14 @@ describe('DatabaseBackupAndRestoreService', () => { const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; - mockFilesCache([dbBackupFile]); + let dbBackupFile = mockFileElement('db.backup'); + mockFilesCacheService([dbBackupFile]); + + let fileUploadService = ngMocks.get(FileUploadService); + when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}), dbBackupFile.id)) + .thenReturn(mustBeConsumedAsyncObservable({ + type: HttpEventType.Response + } as HttpResponse)); // Act let backupPromise = lastValueFrom(databaseBackupAndRestoreService.backup()); @@ -156,13 +160,3 @@ describe('DatabaseBackupAndRestoreService', () => { }); }) }); - -export function mockDatabaseBackupAndRestoreService() { - let databaseBackupAndRestoreService = mock(); - MockInstance(DatabaseBackupAndRestoreService, () => { - return { - backup: databaseBackupAndRestoreService.backup - } - }); - return databaseBackupAndRestoreService; -} diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 3c51ca3..8ea2d50 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -7,7 +7,7 @@ import {FileService} from "../file-list/file.service"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {HttpClient, HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; import {BackgroundTaskService} from "../background-task/background-task.service"; -import {UserRootComponent} from "../user-root/user-root.component"; +import {FilesCacheService} from "../files-cache/files-cache.service"; @Injectable() export class DatabaseBackupAndRestoreService { @@ -15,7 +15,7 @@ export class DatabaseBackupAndRestoreService { private static readonly DB_NAME = 'db.backup'; constructor(private fileUploadService: FileUploadService, private http: HttpClient, - private backgroundTaskService: BackgroundTaskService, private userRootComponent: UserRootComponent) { + private backgroundTaskService: BackgroundTaskService, private filesCacheService: FilesCacheService) { // this.restore().subscribe(); // TODO: check refresh after restore } @@ -62,7 +62,7 @@ export class DatabaseBackupAndRestoreService { } private findExistingDbFile() { - let files = this.userRootComponent.getFilesCache().all; + let files = this.filesCacheService.getAll(); // TODO: ensure there cannot be any conflicts with user files return files.filter(f => isFileElement(f)) .map(f => f as FileElement) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index da1790a..1569ec8 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -35,8 +35,8 @@ import {MatChipGridHarness} from "@angular/material/chips/testing"; import {mockFileService} from "./file.service.spec"; import {MatSortModule} from "@angular/material/sort"; import {BreakpointObserver} from "@angular/cdk/layout"; -import {UserRootComponent} from "../user-root/user-root.component"; -import {mockFilesCache} from "../user-root/user-root.component.spec"; +import {FilesCacheService} from "../files-cache/files-cache.service"; +import {mockFilesCacheService} from "../files-cache/files-cache.service.spec"; function mockRenderAndWaitForChanges() { let fixture = MockRender(FileListComponent, null, {reset: true}); @@ -50,7 +50,7 @@ function mockRenderAndWaitForChanges() { describe('FileListComponent', () => { beforeEach(() => MockBuilder(FileListComponent, AppModule) - .mock(UserRootComponent) + .mock(FilesCacheService) .keep(MatTableModule) .keep(NgxFilesizeModule) .keep(MatMenuModule) @@ -61,17 +61,16 @@ describe('FileListComponent', () => { .keep(MatChipsModule) .keep(MatSortModule) .keep(BreakpointObserver) - // For some reason, we need to explicitly add a provider for UserRootComponent .provide({ - provide: UserRootComponent, - useValue: mock() + provide: FilesCacheService, + useValue: mock() }) .replace(BrowserAnimationsModule, NoopAnimationsModule) ); it('should create (no element)', fakeAsync(() => { // Arrange - mockFilesCache([]); + mockFilesCacheService([], true); // Act const component = mockRenderAndWaitForChanges().point.componentInstance; @@ -136,8 +135,8 @@ describe('FileListComponent', () => { when(() => fileService.trash(el1.id)) .thenReturn(mustBeConsumedAsyncObservable(undefined)); // A refresh is expected - let userRootComponent = ngMocks.get(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.get(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); let fixture = mockRenderAndWaitForChanges(); let page = new Page(fixture); @@ -160,8 +159,8 @@ describe('FileListComponent', () => { when(() => fileService.trash(cat1Folder.id)) .thenReturn(mustBeConsumedAsyncObservable(undefined)); - let userRootComponent = ngMocks.findInstance(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.findInstance(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); // Act mockRenderAndWaitForChanges(); @@ -208,8 +207,8 @@ describe('FileListComponent', () => { let fileService = mockListItemsAndCategories([el2]); // A refresh is expected - let userRootComponent = ngMocks.get(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.get(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); when(() => fileService.findOrCreateFolder('Cat848', 'baseFolderId')).thenReturn(of('cat848Id')); when(() => fileService.setCategory(el2.id, 'cat848Id')).thenReturn(of(undefined)); @@ -270,8 +269,8 @@ describe('FileListComponent', () => { let fileElement = mockFileElement('name1'); let fileService = mockListItemsAndCategories([fileElement]); // A refresh is expected - let userRootComponent = ngMocks.get(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.get(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); when(() => fileService.setCategory(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); @@ -294,8 +293,8 @@ describe('FileListComponent', () => { let fileElement = mockFileElement('name1'); let fileService = mockListItemsAndCategories([fileElement]); // A refresh is expected - let userRootComponent = ngMocks.get(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.get(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); when(() => fileService.setCategory(fileElement.id, "baseFolderId")).thenReturn(of(undefined)); @@ -318,8 +317,8 @@ describe('FileListComponent', () => { let fileElement = mockFileElement('name1'); let fileService = mockListItemsAndCategories([fileElement]); // A refresh is expected - let userRootComponent = ngMocks.get(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.get(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); when(() => fileService.setCategory(fileElement.id, "parentCat45Id")).thenReturn(of(undefined)); @@ -343,8 +342,8 @@ describe('FileListComponent', () => { let el2 = mockFileElement('name2'); let fileService = mockListItemsAndCategories([el2]); // A refresh is expected - let userRootComponent = ngMocks.get(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.get(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); when(() => fileService.setCategory(el2.id, 'cat7Id')).thenReturn(of(undefined)); @@ -415,8 +414,8 @@ describe('FileListComponent', () => { let fileService = mockListItemsAndCategories([cat1Folder, cat2Folder, cat1bFolder, fileElement1], true); // A refresh is expected - let userRootComponent = ngMocks.get(UserRootComponent); - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + let filesCacheService = ngMocks.get(FilesCacheService); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); when(() => fileService.setCategory(fileElement1.id, cat1Folder.id)).thenReturn(of(undefined)); @@ -744,7 +743,7 @@ function mockListItemsAndCategories(itemsAndCategories: (FileElement | FolderEle itemsAndCategories.push(mockFileElement(cat.name + "_file", cat.id)) }) } - mockFilesCache(itemsAndCategories); + mockFilesCacheService(itemsAndCategories, true); return mockFileService(); } diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index 759e460..b8044b0 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -19,7 +19,7 @@ import { MatAutocompleteTrigger } from "@angular/material/autocomplete"; import {MatSort, MatSortable} from "@angular/material/sort"; -import {UserRootComponent} from "../user-root/user-root.component"; +import {FilesCacheService} from "../files-cache/files-cache.service"; export interface FileOrFolderElement { id: string; @@ -64,16 +64,15 @@ export class FileListComponent implements OnInit { private categoryFilters = new Set(); private allFiles: FileOrFolderElement[] = []; - constructor(private fileService: FileService, public dialog: MatDialog, private userRootComponent: UserRootComponent) { + constructor(private fileService: FileService, public dialog: MatDialog, private filesCacheService: FilesCacheService) { this.fileDataSource.filterPredicate = data => { return this.filterPredicate(data); } } ngOnInit(): void { - let filesCache = this.userRootComponent.getFilesCache(); - this.baseFolderId = filesCache.baseFolder; - this.allFiles = filesCache.all; + this.baseFolderId = this.filesCacheService.getBaseFolder(); + this.allFiles = this.filesCacheService.getAll(); this.populateFilesAndCategories(); if (this.fileSort) { @@ -152,7 +151,7 @@ export class FileListComponent implements OnInit { } private refresh() { - this.userRootComponent.refreshCacheAndReload(); + this.filesCacheService.refreshCacheAndReload(); } private populateFilesAndCategories() { diff --git a/src/app/file-upload/file-upload.component.spec.ts b/src/app/file-upload/file-upload.component.spec.ts index e92a497..e4be224 100644 --- a/src/app/file-upload/file-upload.component.spec.ts +++ b/src/app/file-upload/file-upload.component.spec.ts @@ -3,7 +3,7 @@ import {FileUploadComponent} from './file-upload.component'; import {MatIconModule} from "@angular/material/icon"; import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; -import {toFileOrBlob} from "./file-upload.service"; +import {FileUploadService, toFileOrBlob} from "./file-upload.service"; import {mock, when} from "strong-mock"; import {Observable, of} from "rxjs"; import {FileUploadElementComponent} from "./file-upload-element/file-upload-element.component"; @@ -12,19 +12,21 @@ import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; import {HarnessLoader} from "@angular/cdk/testing"; import {MatButtonHarness} from "@angular/material/button/testing"; import {GooglePickerService} from "./google-picker.service"; -import {mockFileUploadService} from "./file-upload.service.spec"; import {BreakpointObserver} from "@angular/cdk/layout"; -import {UserRootComponent} from "../user-root/user-root.component"; +import {FilesCacheService} from "../files-cache/files-cache.service"; describe('FileUploadComponent', () => { beforeEach(() => { return MockBuilder(FileUploadComponent, AppModule) .keep(MatIconModule) .keep(BreakpointObserver) - // For some reason, we need to explicitly add a provider for UserRootComponent .provide({ - provide: UserRootComponent, - useValue: mock() + provide: FilesCacheService, + useValue: mock() + }) + .provide({ + provide: FileUploadService, + useValue: mock() }) }); @@ -40,13 +42,13 @@ describe('FileUploadComponent', () => { describe('When selecting a file to upload', () => { it('Should shows the file as being uploaded', () => { // Arrange - let fileUploadService = mockFileUploadService(); - let file = new File([''], 'TestFile.txt'); - when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(new Observable()) - const fixture = MockRender(FileUploadComponent); const page = new Page(fixture); + let fileUploadService = ngMocks.get(FileUploadService); + let file = new File([''], 'TestFile.txt'); + when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(new Observable()) + // Act page.uploadFile(file); @@ -57,7 +59,10 @@ describe('FileUploadComponent', () => { it('Should update upload progress', () => { // Arrange - let fileUploadService = mockFileUploadService(); + const fixture = MockRender(FileUploadComponent); + const page = new Page(fixture); + + let fileUploadService = ngMocks.get(FileUploadService); let file = new File([''], 'TestFile.txt'); when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(of({ loaded: 50, @@ -65,9 +70,6 @@ describe('FileUploadComponent', () => { type: HttpEventType.UploadProgress })) - const fixture = MockRender(FileUploadComponent); - const page = new Page(fixture); - // Act page.uploadFile(file); @@ -81,18 +83,18 @@ describe('FileUploadComponent', () => { it('Should trigger upload finish event', () => { // Arrange - let fileUploadService = mockFileUploadService(); + const fixture = MockRender(FileUploadComponent); + const page = new Page(fixture); + + let fileUploadService = ngMocks.get(FileUploadService); let file = new File([''], 'TestFile.txt'); when(() => fileUploadService.upload(toFileOrBlob(file))).thenReturn(of({ type: HttpEventType.Response } as HttpResponse)) - const fixture = MockRender(FileUploadComponent); - const page = new Page(fixture); - - let userRootComponent = ngMocks.get(UserRootComponent); + let filesCacheService = ngMocks.get(FilesCacheService); // A page refresh is expected - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); // Act page.uploadFile(file); @@ -112,9 +114,9 @@ describe('FileUploadComponent', () => { const fixture = MockRender(FileUploadComponent); const page = new Page(fixture); - let userRootComponent = ngMocks.get(UserRootComponent); + let filesCacheService = ngMocks.findInstance(FilesCacheService); // A page refresh is expected - when(() => userRootComponent.refreshCacheAndReload()).thenReturn(); + when(() => filesCacheService.refreshCacheAndReload()).thenReturn(); // Act await page.openGooglePicker(); diff --git a/src/app/file-upload/file-upload.component.ts b/src/app/file-upload/file-upload.component.ts index a964ca2..e83a065 100644 --- a/src/app/file-upload/file-upload.component.ts +++ b/src/app/file-upload/file-upload.component.ts @@ -3,7 +3,7 @@ import {FileUploadService, toFileOrBlob} from "./file-upload.service"; import {FileUploadProgress} from "./file-upload-element/file-upload-element.component"; import {HttpEventType} from "@angular/common/http"; import {GooglePickerService} from "./google-picker.service"; -import {UserRootComponent} from "../user-root/user-root.component"; +import {FilesCacheService} from "../files-cache/files-cache.service"; @Component({ selector: 'app-file-upload', @@ -14,7 +14,7 @@ export class FileUploadComponent { files: FileUploadProgress[] = [] - constructor(private fileUploadService: FileUploadService, private googlePickerService: GooglePickerService, private userRootComponent: UserRootComponent) { + constructor(private fileUploadService: FileUploadService, private googlePickerService: GooglePickerService, private filesCacheService: FilesCacheService) { } onFileSelected(event: Event) { @@ -30,7 +30,7 @@ export class FileUploadComponent { async showGooglePicker() { this.googlePickerService.show() - .then(() => this.userRootComponent.refreshCacheAndReload()); + .then(() => this.filesCacheService.refreshCacheAndReload()); } private upload(file: File) { @@ -39,7 +39,7 @@ export class FileUploadComponent { this.fileUploadService.upload(toFileOrBlob(file)) .subscribe(e => { if (e.type === HttpEventType.Response) { - this.userRootComponent.refreshCacheAndReload(); + this.filesCacheService.refreshCacheAndReload(); } else { fileProgress.loaded = e.loaded; if (e.total != null) { diff --git a/src/app/file-upload/file-upload.service.spec.ts b/src/app/file-upload/file-upload.service.spec.ts index 1e5d5cb..4e58629 100644 --- a/src/app/file-upload/file-upload.service.spec.ts +++ b/src/app/file-upload/file-upload.service.spec.ts @@ -1,16 +1,21 @@ import {FileUploadService, toFileOrBlob} from './file-upload.service'; -import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; +import {MockBuilder, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; import {HttpClientModule, HttpEventType, HttpProgressEvent, HttpSentEvent} from "@angular/common/http"; import {fakeAsync, TestBed, tick} from "@angular/core/testing"; -import {mockBaseFolderService} from "./base-folder.service.spec"; import {mock} from "strong-mock"; +import {FilesCacheService} from "../files-cache/files-cache.service"; +import {mockFilesCacheServiceGetBaseFolder} from "../files-cache/files-cache.service.spec"; describe('FileUploadService', () => { beforeEach(() => MockBuilder(FileUploadService, AppModule) .replace(HttpClientModule, HttpClientTestingModule) + .provide({ + provide: FilesCacheService, + useValue: mock() + }) ); it('should be created', () => { @@ -25,8 +30,8 @@ describe('FileUploadService', () => { it('should upload', fakeAsync(() => { // Arrange let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); - mockBaseFolderService() const service = MockRender(FileUploadService).point.componentInstance; + mockFilesCacheServiceGetBaseFolder() let httpTestingController = TestBed.inject(HttpTestingController); // Act @@ -66,8 +71,8 @@ describe('FileUploadService', () => { it('should filter out unwanted http events when uploading', fakeAsync(() => { // Arrange let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); - mockBaseFolderService(); const service = MockRender(FileUploadService).point.componentInstance; + mockFilesCacheServiceGetBaseFolder() let httpTestingController = TestBed.inject(HttpTestingController); // Act @@ -111,8 +116,8 @@ describe('FileUploadService', () => { it('should overwrite an existing file when provided with an id', fakeAsync(() => { // Arrange let f = new File(["test_content"], "test.txt", {type: 'application/txt'}); - mockBaseFolderService() const service = MockRender(FileUploadService).point.componentInstance; + mockFilesCacheServiceGetBaseFolder() let httpTestingController = TestBed.inject(HttpTestingController); // Act @@ -146,12 +151,3 @@ describe('FileUploadService', () => { }) }); -export function mockFileUploadService() { - let fileUploadServiceMock = mock(); - MockInstance(FileUploadService, () => { - return { - upload: fileUploadServiceMock.upload - } - }); - return fileUploadServiceMock; -} diff --git a/src/app/file-upload/file-upload.service.ts b/src/app/file-upload/file-upload.service.ts index 25e7c0f..385b465 100644 --- a/src/app/file-upload/file-upload.service.ts +++ b/src/app/file-upload/file-upload.service.ts @@ -9,7 +9,7 @@ import { HttpResponse } from "@angular/common/http"; import {catchError, filter, mergeMap, Observable, of} from "rxjs"; -import {BaseFolderService} from "./base-folder.service"; +import {FilesCacheService} from "../files-cache/files-cache.service"; export interface FileOrBlob { name: string; @@ -23,32 +23,28 @@ export function toFileOrBlob(file: File) { } } -@Injectable({ - providedIn: 'root' -}) +@Injectable() export class FileUploadService { private readonly DRIVE_API_UPLOAD_FILES_BASE_URL = 'https://www.googleapis.com/upload/drive/v3/files'; - constructor(private http: HttpClient, private baseFolderService: BaseFolderService) { + constructor(private http: HttpClient, private filesCacheService: FilesCacheService) { } upload(file: FileOrBlob, fileId?: string): Observable> { const contentType = file.blob.type || 'application/octet-stream'; - return this.baseFolderService.findOrCreateBaseFolder() - .pipe(mergeMap(baseFolderId => { - return this.createUploadFileRequest(file, contentType, baseFolderId, fileId); - }), - mergeMap(metadataRes => { - const locationUrl = metadataRes.headers.get('Location') ?? ''; - return this.uploadFileToUrl(locationUrl, contentType, file); - }), - filter((e: HttpEvent): e is HttpProgressEvent | HttpResponse => e.type === HttpEventType.UploadProgress || e.type === HttpEventType.Response), - catchError(err => { - console.log(err) - return of(); - })) + let baseFolderId = this.filesCacheService.getBaseFolder(); + return this.createUploadFileRequest(file, contentType, baseFolderId, fileId).pipe( + mergeMap(metadataRes => { + const locationUrl = metadataRes.headers.get('Location') ?? ''; + return this.uploadFileToUrl(locationUrl, contentType, file); + }), + filter((e: HttpEvent): e is HttpProgressEvent | HttpResponse => e.type === HttpEventType.UploadProgress || e.type === HttpEventType.Response), + catchError(err => { + console.log(err) + return of(); + })) } private uploadFileToUrl(url: string, contentType: string, file: FileOrBlob) { diff --git a/src/app/files-cache/files-cache.service.spec.ts b/src/app/files-cache/files-cache.service.spec.ts new file mode 100644 index 0000000..1676da8 --- /dev/null +++ b/src/app/files-cache/files-cache.service.spec.ts @@ -0,0 +1,31 @@ +import {FilesCacheService} from './files-cache.service'; +import {FileOrFolderElement} from "../file-list/file-list.component"; +import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; +import {when} from "strong-mock"; +import {AppModule} from "../app.module"; + +describe('FilesCacheService', () => { + beforeEach(() => MockBuilder(FilesCacheService, AppModule)); + + it('should be created', () => { + // Act + const service = MockRender(FilesCacheService).point.componentInstance; + + // Assert + expect(service).toBeTruthy(); + }); +}); + +export function mockFilesCacheServiceGetBaseFolder() { + let filesCacheService = ngMocks.findInstance(FilesCacheService); + when(() => filesCacheService.getBaseFolder()).thenReturn('baseFolderId') +} + +export function mockFilesCacheService(files: FileOrFolderElement[], mockBaseFolder: boolean = false) { + let filesCacheService = ngMocks.findInstance(FilesCacheService); + when(() => filesCacheService.getAll()).thenReturn(files); + if (mockBaseFolder) { + mockFilesCacheServiceGetBaseFolder(); + } + return filesCacheService; +} diff --git a/src/app/files-cache/files-cache.service.ts b/src/app/files-cache/files-cache.service.ts new file mode 100644 index 0000000..b365200 --- /dev/null +++ b/src/app/files-cache/files-cache.service.ts @@ -0,0 +1,38 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRoute, Router} from "@angular/router"; +import {FilesCache} from "./files.resolver"; + +@Injectable() +export class FilesCacheService { + static reloadRouteData = false; + + constructor(private activatedRoute: ActivatedRoute, private router: Router) { + } + + static shouldReloadRouteData() { + if (FilesCacheService.reloadRouteData) { + FilesCacheService.reloadRouteData = false; + return true; + } + return false; + } + + getBaseFolder() { + return this.getFilesCache().baseFolder; + } + + getAll() { + return this.getFilesCache().all; + } + + refreshCacheAndReload() { + FilesCacheService.reloadRouteData = true; + this.router.navigate([this.router.url], { + onSameUrlNavigation: "reload" + }) + } + + private getFilesCache(): FilesCache { + return this.activatedRoute.snapshot.data["files"]; + } +} diff --git a/src/app/resolver/files.resolver.spec.ts b/src/app/files-cache/files.resolver.spec.ts similarity index 100% rename from src/app/resolver/files.resolver.spec.ts rename to src/app/files-cache/files.resolver.spec.ts diff --git a/src/app/resolver/files.resolver.ts b/src/app/files-cache/files.resolver.ts similarity index 100% rename from src/app/resolver/files.resolver.ts rename to src/app/files-cache/files.resolver.ts diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index 79ddf1f..e04bb83 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -1,21 +1,26 @@ import {Rule, RuleRepository} from "./rule.repository"; -import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; import {db} from "../database/db"; import {mock, when} from "strong-mock"; -import {mockDatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service.spec"; import {dbCleanUp, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {HttpEventType, HttpResponse} from "@angular/common/http"; +import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; function mockBackupCall() { - let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); + let databaseBackupAndRestoreService = ngMocks.get(DatabaseBackupAndRestoreService); return when(() => databaseBackupAndRestoreService.backup()) .thenReturn(mustBeConsumedAsyncObservable({type: HttpEventType.Response} as HttpResponse)); } describe('RuleRepository', () => { - beforeEach(() => MockBuilder(RuleRepository, AppModule)); + beforeEach(() => MockBuilder(RuleRepository, AppModule) + .provide({ + provide: DatabaseBackupAndRestoreService, + useValue: mock() + }) + ); // Db cleanup after each test afterEach(async () => { @@ -33,8 +38,8 @@ describe('RuleRepository', () => { describe('findAll', () => { it('should list two rules', async () => { // Arrange - mockBackupCall().times(2); const ruleRepository = MockRender(RuleRepository).point.componentInstance; + mockBackupCall().times(2); let rule1: Rule = { name: 'TestRule', category: ['Test1', 'ChildTest1'], @@ -70,9 +75,8 @@ describe('RuleRepository', () => { describe('create', () => { it('should persist a new rule', async () => { // Arrange - mockBackupCall(); - const ruleRepository = MockRender(RuleRepository).point.componentInstance; + mockBackupCall(); let rule: Rule = { name: 'TestRule', category: ['Test1', 'ChildTest1'], @@ -97,10 +101,10 @@ describe('RuleRepository', () => { describe('delete', () => { it('should delete one rule', async () => { // Arrange - // 2 calls to 'backup' expected, from create, and then from delete - mockBackupCall().times(2); const ruleRepository = MockRender(RuleRepository).point.componentInstance; + // 2 calls to 'backup' expected, from create, and then from delete + mockBackupCall().times(2); let rule: Rule = { name: 'TestRule', category: ['Test1', 'ChildTest1'], diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 6346b0e..aa99f46 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -1,4 +1,3 @@ -import {UserRootComponent} from "../user-root/user-root.component"; import {RuleService} from './rule.service'; import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; @@ -10,8 +9,9 @@ import {mockFileElement} from "../file-list/file-list.component.spec"; import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; import {FileService} from "../file-list/file.service"; import {getSampleRules} from "./rules.component.spec"; -import {mockFilesCache} from "../user-root/user-root.component.spec"; import {RuleRepository} from "./rule.repository"; +import {FilesCacheService} from "../files-cache/files-cache.service"; +import {mockFilesCacheService} from "../files-cache/files-cache.service.spec"; function mockBillCategoryFindOrCreate(fileService: FileService) { @@ -25,13 +25,12 @@ function mockBillCategoryFindOrCreate(fileService: FileService) { describe('RuleService', () => { beforeEach(() => MockBuilder(RuleService, AppModule) .provide({ - provide: RuleRepository, - useValue: mock() + provide: FilesCacheService, + useValue: mock() }) - // For some reason, we need to explicitly add a provider for UserRootComponent .provide({ - provide: UserRootComponent, - useValue: mock() + provide: RuleRepository, + useValue: mock() }) ); @@ -63,7 +62,7 @@ describe('RuleService', () => { .thenResolve(getSampleRules()); - mockFilesCache([file]); + mockFilesCacheService([file]); // Act service.runAll().subscribe(); @@ -89,7 +88,7 @@ describe('RuleService', () => { .thenResolve(getSampleRules()); let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); - mockFilesCache([file]); + mockFilesCacheService([file]); // Act service.runAll().subscribe(); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index f041690..a5b0495 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -4,18 +4,18 @@ import {filter, from, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {BaseFolderService} from "../file-upload/base-folder.service"; import {Rule, RuleRepository} from "./rule.repository"; -import {UserRootComponent} from "../user-root/user-root.component"; +import {FilesCacheService} from "../files-cache/files-cache.service"; @Injectable() export class RuleService { constructor(private fileService: FileService, private baseFolderService: BaseFolderService, - private ruleRepository: RuleRepository, private userRootComponent: UserRootComponent) { + private ruleRepository: RuleRepository, private filesCacheService: FilesCacheService) { } runAll(): Observable { return from(this.ruleRepository.findAll()) .pipe(mergeMap(rules => { - let fileOrFolders = this.userRootComponent.getFilesCache().all; + let fileOrFolders = this.filesCacheService.getAll() // Get all files let files = fileOrFolders.filter(file => isFileElement(file)) .map(value => value as FileElement); diff --git a/src/app/user-root/user-root.component.spec.ts b/src/app/user-root/user-root.component.spec.ts index 7cbec9c..73c20bf 100644 --- a/src/app/user-root/user-root.component.spec.ts +++ b/src/app/user-root/user-root.component.spec.ts @@ -1,9 +1,6 @@ import {UserRootComponent} from './user-root.component'; -import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; +import {MockBuilder, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; -import {FileOrFolderElement} from "../file-list/file-list.component"; -import {FilesCache} from "../resolver/files.resolver"; -import {when} from "strong-mock"; describe('UserRootComponent', () => { beforeEach(() => MockBuilder(UserRootComponent, AppModule)) @@ -13,12 +10,3 @@ describe('UserRootComponent', () => { expect(component).toBeTruthy(); }); }); - -export function mockFilesCache(files: FileOrFolderElement[]) { - let filesCache: FilesCache = { - baseFolder: 'baseFolderId', - all: files - }; - let userRootComponent = ngMocks.findInstance(UserRootComponent); - when(() => userRootComponent.getFilesCache()).thenReturn(filesCache); -} diff --git a/src/app/user-root/user-root.component.ts b/src/app/user-root/user-root.component.ts index e35a42f..29b0ed2 100644 --- a/src/app/user-root/user-root.component.ts +++ b/src/app/user-root/user-root.component.ts @@ -1,37 +1,16 @@ import {Component} from '@angular/core'; -import {ActivatedRoute, Router} from "@angular/router"; -import {FilesCache} from "../resolver/files.resolver"; import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; import {RuleRepository} from "../rules/rule.repository"; +import {FileUploadService} from "../file-upload/file-upload.service"; +import {FilesCacheService} from "../files-cache/files-cache.service"; @Component({ selector: 'app-user-root', templateUrl: './user-root.component.html', styleUrls: ['./user-root.component.scss'], - providers: [RuleRepository, DatabaseBackupAndRestoreService] + providers: [RuleRepository, DatabaseBackupAndRestoreService, FileUploadService, FilesCacheService] }) export class UserRootComponent { - static reloadRouteData = false; - - constructor(private activatedRoute: ActivatedRoute, private router: Router) { - } - - static shouldReloadRouteData() { - if (UserRootComponent.reloadRouteData) { - UserRootComponent.reloadRouteData = false; - return true; - } - return false; - } - - getFilesCache(): FilesCache { - return this.activatedRoute.snapshot.data["files"]; - } - - refreshCacheAndReload() { - UserRootComponent.reloadRouteData = true; - this.router.navigate([this.router.url], { - onSameUrlNavigation: "reload" - }) + constructor() { } } From cabc886b67acbee7658bf3245479cc07a64c2e9b Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 11 Jan 2024 14:27:56 +0100 Subject: [PATCH 31/66] [us40] RuleService: use cache instead of baseFolderService.findOrCreateBaseFolder --- src/app/file-upload/base-folder.service.ts | 1 - src/app/rules/rule.service.spec.ts | 10 ++----- src/app/rules/rule.service.ts | 34 ++++++++++------------ 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/app/file-upload/base-folder.service.ts b/src/app/file-upload/base-folder.service.ts index 8bcc1c0..8cfecbe 100644 --- a/src/app/file-upload/base-folder.service.ts +++ b/src/app/file-upload/base-folder.service.ts @@ -10,7 +10,6 @@ export class BaseFolderService { constructor(private fileService: FileService) { } - // TODO: persist the base folder id for quick access findOrCreateBaseFolder() { return this.fileService.findOrCreateFolder(BaseFolderService.BASE_FOLDER_NAME); } diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index aa99f46..cd33f04 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -6,7 +6,6 @@ import {mock, when} from "strong-mock"; import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; import {fakeAsync, tick} from "@angular/core/testing"; import {mockFileElement} from "../file-list/file-list.component.spec"; -import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; import {FileService} from "../file-list/file.service"; import {getSampleRules} from "./rules.component.spec"; import {RuleRepository} from "./rule.repository"; @@ -45,8 +44,6 @@ describe('RuleService', () => { describe('runAll', () => { it('should automatically categorize a file', fakeAsync(() => { // Arrange - mockBaseFolderService(); - let fileService = mockFileService(); mockBillCategoryFindOrCreate(fileService); @@ -61,8 +58,7 @@ describe('RuleService', () => { when(() => ruleRepository.findAll()) .thenResolve(getSampleRules()); - - mockFilesCacheService([file]); + mockFilesCacheService([file], true); // Act service.runAll().subscribe(); @@ -74,8 +70,6 @@ describe('RuleService', () => { it('should not categorize a file which is already in the correct category', fakeAsync(() => { // Arrange - mockBaseFolderService(); - let fileService = mockFileService(); mockBillCategoryFindOrCreate(fileService); @@ -88,7 +82,7 @@ describe('RuleService', () => { .thenResolve(getSampleRules()); let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); - mockFilesCacheService([file]); + mockFilesCacheService([file], true); // Act service.runAll().subscribe(); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index a5b0495..d5b844e 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -2,14 +2,12 @@ import {Injectable} from '@angular/core'; import {FileService} from "../file-list/file.service"; import {filter, from, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; -import {BaseFolderService} from "../file-upload/base-folder.service"; import {Rule, RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; @Injectable() export class RuleService { - constructor(private fileService: FileService, private baseFolderService: BaseFolderService, - private ruleRepository: RuleRepository, private filesCacheService: FilesCacheService) { + constructor(private fileService: FileService, private ruleRepository: RuleRepository, private filesCacheService: FilesCacheService) { } runAll(): Observable { @@ -67,22 +65,20 @@ export class RuleService { * Find or create the categories for each file and associate them */ private setAllFileCategory(fileToCategoryMap: Map) { - return this.baseFolderService.findOrCreateBaseFolder() - .pipe(mergeMap(baseFolderId => { - let categoryRequests: Observable[] = []; - fileToCategoryMap - .forEach((category, file) => { - categoryRequests.push(this.findOrCreateCategories(category, baseFolderId) - // There is no need to set the category if the current category is correct - .pipe(filter(categoryId => file.parentId !== categoryId), - mergeMap(categoryId => { - return this.fileService.setCategory(file.id, categoryId); - }))); - }); - let observable = zip(categoryRequests); - return observable - .pipe(map(() => { - })); + let baseFolderId = this.filesCacheService.getBaseFolder(); + let categoryRequests: Observable[] = []; + fileToCategoryMap + .forEach((category, file) => { + categoryRequests.push(this.findOrCreateCategories(category, baseFolderId) + // There is no need to set the category if the current category is correct + .pipe(filter(categoryId => file.parentId !== categoryId), + mergeMap(categoryId => { + return this.fileService.setCategory(file.id, categoryId); + }))); + }); + let observable = zip(categoryRequests); + return observable + .pipe(map(() => { })); } From d9910404c2b6e09bb32194678dea3cc63b3718f9 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 11 Jan 2024 15:19:07 +0100 Subject: [PATCH 32/66] [us40] Fix baseFolderService not used anywhere but in resolver causing unexpected issue --- src/app/file-list/file.service.spec.ts | 27 +++++++++- src/app/file-list/file.service.ts | 8 ++- .../file-upload/base-folder.service.spec.ts | 50 ------------------- src/app/file-upload/base-folder.service.ts | 16 ------ src/app/files-cache/files.resolver.spec.ts | 3 +- src/app/files-cache/files.resolver.ts | 4 +- 6 files changed, 33 insertions(+), 75 deletions(-) delete mode 100644 src/app/file-upload/base-folder.service.spec.ts delete mode 100644 src/app/file-upload/base-folder.service.ts diff --git a/src/app/file-list/file.service.spec.ts b/src/app/file-list/file.service.spec.ts index b940d11..0332708 100644 --- a/src/app/file-list/file.service.spec.ts +++ b/src/app/file-list/file.service.spec.ts @@ -5,7 +5,8 @@ import {HttpClientModule} from "@angular/common/http"; import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; import {fakeAsync, TestBed, tick} from "@angular/core/testing"; import {FileElement, FileOrFolderElement, FolderElement} from "./file-list.component"; -import {mock} from "strong-mock"; +import {mock, when} from "strong-mock"; +import {of} from "rxjs"; describe('FileService', () => { beforeEach(() => MockBuilder(FileService, AppModule) @@ -302,6 +303,27 @@ describe('FileService', () => { })) }); }) + + describe('findOrCreateBaseFolder', function () { + it('should find or create base folder', fakeAsync(() => { + // Arrange + let fileServiceMock = mockFileService(); + + when(() => fileServiceMock.findOrCreateFolder('storemydocs.ovh')) + .thenReturn(of('folderId51')) + const service = MockRender(FileService).point.componentInstance; + service.findOrCreateFolder = fileServiceMock.findOrCreateFolder; + + // Act + let result = ''; + service.findOrCreateBaseFolder() + .subscribe(value => result = value); + + // Assert + tick(); + expect(result).toBe('folderId51'); + })) + }); }); export function mockFileService() { @@ -311,7 +333,8 @@ export function mockFileService() { findOrCreateFolder: fileServiceMock.findOrCreateFolder, findAll: fileServiceMock.findAll, trash: fileServiceMock.trash, - setCategory: fileServiceMock.setCategory + setCategory: fileServiceMock.setCategory, + findOrCreateBaseFolder: fileServiceMock.findOrCreateBaseFolder } }); return fileServiceMock; diff --git a/src/app/file-list/file.service.ts b/src/app/file-list/file.service.ts index b96353f..2ee09cd 100644 --- a/src/app/file-list/file.service.ts +++ b/src/app/file-list/file.service.ts @@ -2,17 +2,21 @@ import {Injectable} from '@angular/core'; import {FileElement, FileOrFolderElement, FolderElement} from "./file-list.component"; import {map, mergeMap, Observable, of} from "rxjs"; import {HttpClient} from "@angular/common/http"; -import {BaseFolderService} from "../file-upload/base-folder.service"; @Injectable({ providedIn: 'root' }) export class FileService { + static readonly BASE_FOLDER_NAME = 'storemydocs.ovh'; static readonly DRIVE_API_FILES_BASE_URL = 'https://www.googleapis.com/drive/v3/files'; constructor(private http: HttpClient) { } + findOrCreateBaseFolder() { + return this.findOrCreateFolder(FileService.BASE_FOLDER_NAME); + } + /** * Return all files managed by the app, except for the base folder */ @@ -22,7 +26,7 @@ export class FileService { if (res.files) { return res.files // Filter out the base folder - .filter(f => f.name !== BaseFolderService.BASE_FOLDER_NAME) + .filter(f => f.name !== FileService.BASE_FOLDER_NAME) .map(f => { if (f.mimeType == 'application/vnd.google-apps.folder') { return { diff --git a/src/app/file-upload/base-folder.service.spec.ts b/src/app/file-upload/base-folder.service.spec.ts deleted file mode 100644 index bfa0508..0000000 --- a/src/app/file-upload/base-folder.service.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {BaseFolderService} from './base-folder.service'; -import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; -import {AppModule} from "../app.module"; -import {fakeAsync, tick} from "@angular/core/testing"; -import {mock, when} from "strong-mock"; -import {of} from "rxjs"; -import {mockFileService} from "../file-list/file.service.spec"; -import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; - -describe('BaseFolderService', () => { - beforeEach(() => MockBuilder(BaseFolderService, AppModule)); - - it('should be created', () => { - // Act - const service = MockRender(BaseFolderService).point.componentInstance; - - // Assert - expect(service).toBeTruthy(); - }); - - it('should find or create base folder', fakeAsync(() => { - // Arrange - let fileServiceMock = mockFileService(); - - when(() => fileServiceMock.findOrCreateFolder('storemydocs.ovh')) - .thenReturn(of('folderId51')) - const service = MockRender(BaseFolderService).point.componentInstance; - - // Act - let result = ''; - service.findOrCreateBaseFolder() - .subscribe(value => result = value); - - // Assert - tick(); - expect(result).toBe('folderId51'); - })); -}); - -export function mockBaseFolderService() { - let baseFolderService = mock(); - MockInstance(BaseFolderService, () => { - return { - findOrCreateBaseFolder: baseFolderService.findOrCreateBaseFolder - } - }); - when(() => baseFolderService.findOrCreateBaseFolder()) - .thenReturn(mustBeConsumedAsyncObservable('baseFolderId')); - return baseFolderService; -} diff --git a/src/app/file-upload/base-folder.service.ts b/src/app/file-upload/base-folder.service.ts deleted file mode 100644 index 8cfecbe..0000000 --- a/src/app/file-upload/base-folder.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Injectable} from '@angular/core'; -import {FileService} from "../file-list/file.service"; - -@Injectable({ - providedIn: 'root' -}) -export class BaseFolderService { - public static readonly BASE_FOLDER_NAME = 'storemydocs.ovh'; - - constructor(private fileService: FileService) { - } - - findOrCreateBaseFolder() { - return this.fileService.findOrCreateFolder(BaseFolderService.BASE_FOLDER_NAME); - } -} diff --git a/src/app/files-cache/files.resolver.spec.ts b/src/app/files-cache/files.resolver.spec.ts index c044903..cd423c8 100644 --- a/src/app/files-cache/files.resolver.spec.ts +++ b/src/app/files-cache/files.resolver.spec.ts @@ -5,7 +5,6 @@ import {MockBuilder, NG_MOCKS_GUARDS, NG_MOCKS_RESOLVERS, ngMocks, Type} from "n import {RouterTestingModule} from "@angular/router/testing"; import {AppModule} from "../app.module"; import {fakeAsync} from "@angular/core/testing"; -import {mockBaseFolderService} from "../file-upload/base-folder.service.spec"; import {when} from "strong-mock"; import {mockFileService} from "../file-list/file.service.spec"; import {of} from "rxjs"; @@ -38,10 +37,10 @@ describe('filesResolver', () => { it('should fetch baseFolder and files', fakeAsync(() => { // Arrange - mockBaseFolderService(); let fileService = mockFileService(); let fileElement = mockFileElement('file1'); when(() => fileService.findAll()).thenReturn(of([fileElement])) + when(() => fileService.findOrCreateBaseFolder()).thenReturn(of('baseFolderId')); // Act navigateTo('/'); diff --git a/src/app/files-cache/files.resolver.ts b/src/app/files-cache/files.resolver.ts index 3f2150b..f7b4701 100644 --- a/src/app/files-cache/files.resolver.ts +++ b/src/app/files-cache/files.resolver.ts @@ -1,6 +1,5 @@ import {ResolveFn} from '@angular/router'; import {inject} from "@angular/core"; -import {BaseFolderService} from "../file-upload/base-folder.service"; import {map, Observable, zip} from "rxjs"; import {FileService} from "../file-list/file.service"; import {FileOrFolderElement} from "../file-list/file-list.component"; @@ -11,10 +10,9 @@ export interface FilesCache { } export const filesResolver: ResolveFn> = (route, state) => { - let baseFolderService = inject(BaseFolderService); let fileService = inject(FileService); return zip( - baseFolderService.findOrCreateBaseFolder(), + fileService.findOrCreateBaseFolder(), fileService.findAll() ).pipe(map(([baseFolderId, allFiles]) => { return { From 35c6e723b353f75c06b06fe51062c1e9ac88acbf Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 11 Jan 2024 16:32:56 +0100 Subject: [PATCH 33/66] [us40] Add missing tests for FilesCacheService.get* --- .../files-cache/files-cache.service.spec.ts | 59 ++++++++++++++++++- src/app/files-cache/files-cache.service.ts | 1 + 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/app/files-cache/files-cache.service.spec.ts b/src/app/files-cache/files-cache.service.spec.ts index 1676da8..0ac81c9 100644 --- a/src/app/files-cache/files-cache.service.spec.ts +++ b/src/app/files-cache/files-cache.service.spec.ts @@ -1,11 +1,21 @@ import {FilesCacheService} from './files-cache.service'; import {FileOrFolderElement} from "../file-list/file-list.component"; import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; -import {when} from "strong-mock"; +import {mock, UnexpectedProperty, when} from "strong-mock"; import {AppModule} from "../app.module"; +import {mockFileElement} from "../file-list/file-list.component.spec"; +import {ActivatedRoute, ActivatedRouteSnapshot, Data} from "@angular/router"; +import {FilesCache} from "./files.resolver"; describe('FilesCacheService', () => { - beforeEach(() => MockBuilder(FilesCacheService, AppModule)); + beforeEach(() => MockBuilder(FilesCacheService, AppModule) + .provide({ + provide: ActivatedRoute, + useValue: mock({ + unexpectedProperty: UnexpectedProperty.THROW + }) + }) + ); it('should be created', () => { // Act @@ -14,6 +24,51 @@ describe('FilesCacheService', () => { // Assert expect(service).toBeTruthy(); }); + + describe('getAll', () => { + it('should return all files from cache', () => { + // Arrange + const service = MockRender(FilesCacheService).point.componentInstance; + + let fileElement = mockFileElement('inCache'); + let filesCache: FilesCache = { + all: [fileElement], + baseFolder: '' + }; + + let activatedRoute = ngMocks.get(ActivatedRoute); + when(() => activatedRoute.snapshot).thenReturn({data: {files: filesCache} as Data} as ActivatedRouteSnapshot); + + // Act + let all = service.getAll(); + + // Assert + expect(all).toEqual([fileElement]) + }) + }) + + describe('getBaseFolder', () => { + it('should return all files from cache', () => { + // Arrange + const service = MockRender(FilesCacheService).point.componentInstance; + + let fileElement = mockFileElement('inCache'); + let filesCache: FilesCache = { + all: [fileElement], + baseFolder: 'baseFolderId' + }; + + let activatedRoute = ngMocks.get(ActivatedRoute); + when(() => activatedRoute.snapshot).thenReturn({data: {files: filesCache} as Data} as ActivatedRouteSnapshot); + + // Act + let baseFolder = service.getBaseFolder(); + + // Assert + expect(baseFolder).toEqual('baseFolderId') + }) + }) + }); export function mockFilesCacheServiceGetBaseFolder() { diff --git a/src/app/files-cache/files-cache.service.ts b/src/app/files-cache/files-cache.service.ts index b365200..8f2a26c 100644 --- a/src/app/files-cache/files-cache.service.ts +++ b/src/app/files-cache/files-cache.service.ts @@ -7,6 +7,7 @@ export class FilesCacheService { static reloadRouteData = false; constructor(private activatedRoute: ActivatedRoute, private router: Router) { + console.log(activatedRoute) } static shouldReloadRouteData() { From be8269689c4b28ffeaecf8e14be2b4e8c8c8d505 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 11 Jan 2024 18:19:58 +0100 Subject: [PATCH 34/66] [us40] Automatic restore when logged in + Fix progress display of background task --- src/app/app-routing.module.spec.ts | 13 ++++++++- .../background-task.service.spec.ts | 6 ++--- .../background-task.service.ts | 10 ++++++- .../progress-indicator.snack-bar.html | 6 ++--- ...atabase-backup-and-restore.service.spec.ts | 19 ++++++++++--- .../database-backup-and-restore.service.ts | 4 +-- src/app/user-root/user-root.component.spec.ts | 27 +++++++++++++++---- src/app/user-root/user-root.component.ts | 3 ++- 8 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/app/app-routing.module.spec.ts b/src/app/app-routing.module.spec.ts index 3b65e25..6f4fb9a 100644 --- a/src/app/app-routing.module.spec.ts +++ b/src/app/app-routing.module.spec.ts @@ -8,7 +8,9 @@ import {GoogleDriveAuthService} from "./file-upload/google-drive-auth.service"; import {mock, when} from "strong-mock"; import {HomepageComponent} from "./homepage/homepage.component"; import {UserRootComponent} from "./user-root/user-root.component"; -import {navigateTo} from "../testing/common-testing-function.spec"; +import {mustBeConsumedAsyncObservable, navigateTo} from "../testing/common-testing-function.spec"; +import {DatabaseBackupAndRestoreService} from "./database/database-backup-and-restore.service"; +import {mockDatabaseBackupAndRestoreService} from "./database/database-backup-and-restore.service.spec"; describe('AppRoutingModule', () => { beforeEach(() => { @@ -19,10 +21,19 @@ describe('AppRoutingModule', () => { ], AppModule, ) + .mock(DatabaseBackupAndRestoreService) // We use the real UserRootComponent as we need it to load its children route .keep(UserRootComponent) .exclude(NG_MOCKS_RESOLVERS) }); + + beforeEach(() => { + // Since we use the real UserRootComponent, we need to mock the automatic restore + let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); + when(() => databaseBackupAndRestoreService.restore()) + .thenReturn(mustBeConsumedAsyncObservable(undefined)); + }) + describe('when logged in', () => { beforeEach(() => { // Mock that the user is logged in diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index c3f59dc..7a2a6a1 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -40,19 +40,19 @@ describe('BackgroundTaskService', () => { // Arrange let fixture = MockRender(BackgroundTaskService); const backgroundTaskService = fixture.point.componentInstance; - let progress = backgroundTaskService.showProgress("Test", "Doing first test", 3); + let progress = backgroundTaskService.showProgress("Test", "Doing first test", 4); // Act progress.next({ index: 2, - value: 25, + value: 50, description: "Doing more test" }) // Assert fixture.detectChanges(); let result = await Page.getProgressMessage(); - expect(result).toEqual("2/3 25% Test: Doing more test..."); + expect(result).toEqual("2/4 37% Test: Doing more test..."); }) it('Should show as completed and should dismiss after 3s', fakeAsync(async () => { diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index c911fef..e769ace 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -87,9 +87,17 @@ class SnackBarProgressIndicatorComponent { data.progress.subscribe(progress => { this.progress = progress; - if (progress.value === 100) { + if (this.isFinished()) { snackBarRef._dismissAfter(3000); } }) } + + getTotalProgress() { + return Math.floor(((this.progress.index - 1) * 100 + this.progress.value) / this.data.stepAmount); + } + + isFinished() { + return this.progress.index === this.data.stepAmount && this.progress.value === 100; + } } diff --git a/src/app/background-task/progress-indicator.snack-bar.html b/src/app/background-task/progress-indicator.snack-bar.html index 9c6a261..0161887 100644 --- a/src/app/background-task/progress-indicator.snack-bar.html +++ b/src/app/background-task/progress-indicator.snack-bar.html @@ -1,5 +1,5 @@ - {{ progress.index }}/{{ data.stepAmount }} {{ progress.value }}% {{ data.globalDescription }} - : {{ progress.description }}... - finished! + {{ progress.index }}/{{ data.stepAmount }} {{ getTotalProgress() }}% {{ data.globalDescription }} + : {{ progress.description }}... + finished! diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 818f97c..0f8e6c1 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -1,5 +1,5 @@ import {DatabaseBackupAndRestoreService} from './database-backup-and-restore.service'; -import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; +import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; import {It, mock, when} from "strong-mock"; import {dbCleanUp, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; @@ -42,7 +42,7 @@ describe('DatabaseBackupAndRestoreService', () => { }); describe('restore', () => { - it('The database should be automatically restored', fakeAsync(async () => { + it('The database should be restored', fakeAsync(async () => { // Arrange let backgroundTaskService = mockBackgroundTaskService(); @@ -50,6 +50,7 @@ describe('DatabaseBackupAndRestoreService', () => { when(() => backgroundTaskService.showProgress("Automatic restore", "Downloading last backup", 2)) .thenReturn(progress); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); + when(() => progress.next({index: 2, value: 0, description: "Importing last backup"})).thenReturn(); when(() => progress.next({index: 2, value: 100})).thenReturn(); let fixture = MockRender(DatabaseBackupAndRestoreService); let databaseBackupAndRestoreService = fixture.point.componentInstance; @@ -110,7 +111,7 @@ describe('DatabaseBackupAndRestoreService', () => { let progress = mock>(); when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) .thenReturn(progress); - when(() => progress.next({index: 2, description: "Uploading backup", value: 50})).thenReturn(); + when(() => progress.next({index: 2, description: "Uploading backup", value: 0})).thenReturn(); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -137,7 +138,7 @@ describe('DatabaseBackupAndRestoreService', () => { let progress = mock>(); when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) .thenReturn(progress); - when(() => progress.next({index: 2, description: "Uploading backup", value: 50})).thenReturn(); + when(() => progress.next({index: 2, description: "Uploading backup", value: 0})).thenReturn(); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -160,3 +161,13 @@ describe('DatabaseBackupAndRestoreService', () => { }); }) }); + +export function mockDatabaseBackupAndRestoreService() { + let databaseBackupAndRestoreService = mock(); + MockInstance(DatabaseBackupAndRestoreService, () => { + return { + restore: databaseBackupAndRestoreService.restore + }; + }) + return databaseBackupAndRestoreService; +} diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 8ea2d50..69fc412 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -16,7 +16,6 @@ export class DatabaseBackupAndRestoreService { constructor(private fileUploadService: FileUploadService, private http: HttpClient, private backgroundTaskService: BackgroundTaskService, private filesCacheService: FilesCacheService) { - // this.restore().subscribe(); // TODO: check refresh after restore } @@ -24,7 +23,7 @@ export class DatabaseBackupAndRestoreService { let progress = this.backgroundTaskService.showProgress('Backup', "Creating backup", 2); return from(exportDB(db)) - .pipe(tap(() => progress.next({index: 2, value: 50, description: "Uploading backup"})), + .pipe(tap(() => progress.next({index: 2, value: 0, description: "Uploading backup"})), mergeMap(blob => { let dbFile = this.findExistingDbFile(); return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); @@ -51,6 +50,7 @@ export class DatabaseBackupAndRestoreService { return of(); } }), + tap(() => progress.next({index: 2, value: 0, description: 'Importing last backup'})), mergeMap(dbDownloadResponse => { return from(importDB(dbDownloadResponse)); }), diff --git a/src/app/user-root/user-root.component.spec.ts b/src/app/user-root/user-root.component.spec.ts index 73c20bf..6325a53 100644 --- a/src/app/user-root/user-root.component.spec.ts +++ b/src/app/user-root/user-root.component.spec.ts @@ -1,12 +1,29 @@ import {UserRootComponent} from './user-root.component'; import {MockBuilder, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; +import {when} from "strong-mock"; +import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; +import {mockDatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service.spec"; +import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; +import {fakeAsync, tick} from "@angular/core/testing"; + describe('UserRootComponent', () => { - beforeEach(() => MockBuilder(UserRootComponent, AppModule)) + beforeEach(() => MockBuilder(UserRootComponent, AppModule) + .mock(DatabaseBackupAndRestoreService) + ) + + it('should restore automatically', fakeAsync(() => { + // Arrange + let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); + when(() => databaseBackupAndRestoreService.restore()) + .thenReturn(mustBeConsumedAsyncObservable(undefined)); + + // Act + MockRender(UserRootComponent); - it('should create', () => { - let component = MockRender(UserRootComponent).point.componentInstance; - expect(component).toBeTruthy(); - }); + // Assert + tick(); + // No failure in mock setup + })); }); diff --git a/src/app/user-root/user-root.component.ts b/src/app/user-root/user-root.component.ts index 29b0ed2..77b3950 100644 --- a/src/app/user-root/user-root.component.ts +++ b/src/app/user-root/user-root.component.ts @@ -11,6 +11,7 @@ import {FilesCacheService} from "../files-cache/files-cache.service"; providers: [RuleRepository, DatabaseBackupAndRestoreService, FileUploadService, FilesCacheService] }) export class UserRootComponent { - constructor() { + constructor(databaseBackupAndRestoreService: DatabaseBackupAndRestoreService) { + databaseBackupAndRestoreService.restore().subscribe(); } } From ef04c3e2f2cab317f1eae976ac769e33fbf6218f Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 12 Jan 2024 10:14:42 +0100 Subject: [PATCH 35/66] [us40] Restore database even if there is conflicting data --- ...atabase-backup-and-restore.service.spec.ts | 109 +++++++++++++----- .../database-backup-and-restore.service.ts | 4 +- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 0f8e6c1..33274cc 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -14,6 +14,44 @@ import {Progress} from "../background-task/background-task.service"; import {mockFilesCacheService} from "../files-cache/files-cache.service.spec"; import {FileUploadService} from "../file-upload/file-upload.service"; import {FilesCacheService} from "../files-cache/files-cache.service"; +import {FileElement} from "../file-list/file-list.component"; +import {Rule} from "../rules/rule.repository"; + +function setupMockForRestore() { + let backgroundTaskService = mockBackgroundTaskService(); + + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Automatic restore", "Downloading last backup", 2)) + .thenReturn(progress); + when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); + when(() => progress.next({index: 2, value: 0, description: "Importing last backup"})).thenReturn(); + when(() => progress.next({index: 2, value: 100})).thenReturn(); +} + +function mockLastBackupDownload(dbBackupFile: FileElement) { + let httpTestingController = TestBed.inject(HttpTestingController); + let request = httpTestingController.expectOne('https://www.googleapis.com/drive/v3/files/' + dbBackupFile.id + '?alt=media'); + request.flush(new Blob([JSON.stringify({ + "formatName": "dexie", + "formatVersion": 1, + "data": { + "databaseName": "StoreMyDocsDB", + "databaseVersion": 3, + "tables": [{"name": "rules", "schema": "++id", "rowCount": 1}], + "data": [{ + "tableName": "rules", + "inbound": true, + "rows": [{ + "name": "TestRule", + "category": ["Test1", "ChildTest1"], + "script": "return true", + "id": 1, + "$types": {"category": "arrayNonindexKeys"} + }] + }] + } + })])); +} describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) @@ -44,16 +82,8 @@ describe('DatabaseBackupAndRestoreService', () => { describe('restore', () => { it('The database should be restored', fakeAsync(async () => { // Arrange - let backgroundTaskService = mockBackgroundTaskService(); - - let progress = mock>(); - when(() => backgroundTaskService.showProgress("Automatic restore", "Downloading last backup", 2)) - .thenReturn(progress); - when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); - when(() => progress.next({index: 2, value: 0, description: "Importing last backup"})).thenReturn(); - when(() => progress.next({index: 2, value: 100})).thenReturn(); - let fixture = MockRender(DatabaseBackupAndRestoreService); - let databaseBackupAndRestoreService = fixture.point.componentInstance; + setupMockForRestore(); + let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; let dbBackupFile = mockFileElement('db.backup'); mockFilesCacheService([dbBackupFile]); @@ -63,29 +93,45 @@ describe('DatabaseBackupAndRestoreService', () => { // Assert tick(); + mockLastBackupDownload(dbBackupFile); + // We need to explicitly wait for the restore to finish + await restorePromise; + + let rules = await db.rules.toArray(); + expect(rules) + .toEqual([{ + id: 1, + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }]); + let httpTestingController = TestBed.inject(HttpTestingController); + httpTestingController.verify(); + })); - let request = httpTestingController.expectOne('https://www.googleapis.com/drive/v3/files/' + dbBackupFile.id + '?alt=media'); - request.flush(new Blob([JSON.stringify({ - "formatName": "dexie", - "formatVersion": 1, - "data": { - "databaseName": "StoreMyDocsDB", - "databaseVersion": 3, - "tables": [{"name": "rules", "schema": "++id", "rowCount": 1}], - "data": [{ - "tableName": "rules", - "inbound": true, - "rows": [{ - "name": "TestRule", - "category": ["Test1", "ChildTest1"], - "script": "return true", - "id": 1, - "$types": {"category": "arrayNonindexKeys"} - }] - }] - } - })])); + it('The database should be restored even if there is existing conflicting on old data', fakeAsync(async () => { + // Arrange + setupMockForRestore(); + + let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + + let dbBackupFile = mockFileElement('db.backup'); + mockFilesCacheService([dbBackupFile]); + let oldRule: Rule = { + id: 1, + name: 'OldTestRule', + category: ['Test1', 'ChildTest1'], + script: 'return old' + }; + db.rules.add(oldRule) + + // Act + let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore()); + + // Assert + tick(); + mockLastBackupDownload(dbBackupFile); // We need to explicitly wait for the restore to finish await restorePromise; @@ -99,6 +145,7 @@ describe('DatabaseBackupAndRestoreService', () => { script: 'return true' }]); + let httpTestingController = TestBed.inject(HttpTestingController); httpTestingController.verify(); })); }) diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 69fc412..b2bbe8c 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -1,5 +1,5 @@ import {Injectable} from '@angular/core'; -import {exportDB, importDB} from "dexie-export-import"; +import {exportDB} from "dexie-export-import"; import {db} from "./db"; import {FileUploadService} from "../file-upload/file-upload.service"; import {filter, from, last, map, mergeMap, Observable, of, tap} from "rxjs"; @@ -52,7 +52,7 @@ export class DatabaseBackupAndRestoreService { }), tap(() => progress.next({index: 2, value: 0, description: 'Importing last backup'})), mergeMap(dbDownloadResponse => { - return from(importDB(dbDownloadResponse)); + return from(db.import(dbDownloadResponse, {clearTablesBeforeImport: true})); }), tap(() => progress.next({index: 2, value: 100})), map(() => void 0)); From 708c33b04bce35415b2fb3c8309000831b139adf Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 12 Jan 2024 13:39:19 +0100 Subject: [PATCH 36/66] [us40] Do not restore the database if it is already up-to-date --- ...atabase-backup-and-restore.service.spec.ts | 47 ++++++++++++++++++- .../database-backup-and-restore.service.ts | 32 ++++++++++--- src/app/file-list/file-list.component.html | 2 +- src/app/file-list/file-list.component.spec.ts | 8 ++-- src/app/file-list/file-list.component.ts | 3 +- src/app/file-list/file.service.spec.ts | 18 +++++-- src/app/file-list/file.service.ts | 8 ++-- .../google-drive-auth.service.spec.ts | 10 +--- src/app/files-cache/files-cache.service.ts | 1 - src/testing/common-testing-function.spec.ts | 9 ++++ 10 files changed, 108 insertions(+), 30 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 33274cc..9b91627 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -2,7 +2,11 @@ import {DatabaseBackupAndRestoreService} from './database-backup-and-restore.ser import {MockBuilder, MockInstance, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; import {It, mock, when} from "strong-mock"; -import {dbCleanUp, mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; +import { + dbCleanUp, + getLocalStorageMock, + mustBeConsumedAsyncObservable +} from "../../testing/common-testing-function.spec"; import {HttpClientModule, HttpEventType, HttpResponse} from "@angular/common/http"; import {mockFileElement} from "../file-list/file-list.component.spec"; import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; @@ -82,6 +86,10 @@ describe('DatabaseBackupAndRestoreService', () => { describe('restore', () => { it('The database should be restored', fakeAsync(async () => { // Arrange + let localStorageMock = getLocalStorageMock(); + when(() => localStorageMock.getItem('last_db_backup_time')).thenReturn(null); + when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); + setupMockForRestore(); let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -110,8 +118,11 @@ describe('DatabaseBackupAndRestoreService', () => { httpTestingController.verify(); })); - it('The database should be restored even if there is existing conflicting on old data', fakeAsync(async () => { + it('The database should be restored even if there is existing and conflicting old data', fakeAsync(async () => { // Arrange + let localStorageMock = getLocalStorageMock(); + when(() => localStorageMock.getItem('last_db_backup_time')).thenReturn(null); + when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); setupMockForRestore(); let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -148,12 +159,41 @@ describe('DatabaseBackupAndRestoreService', () => { let httpTestingController = TestBed.inject(HttpTestingController); httpTestingController.verify(); })); + + it('The database should not be restored if it is already up-to-date', fakeAsync(async () => { + // Arrange + mockBackgroundTaskService(); + let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + + let localStorageMock = getLocalStorageMock(); + // Last db backup is one second later as it will generally be the case + when(() => localStorageMock.getItem('last_db_backup_time')) + .thenReturn('2024-01-09T17:53:08.560Z'); + let dbBackupFile = mockFileElement('db.backup'); + dbBackupFile.modifiedTime = new Date('2024-01-09T17:53:07.560Z') + mockFilesCacheService([dbBackupFile]); + + // Act + let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore(), {defaultValue: undefined}); + + // Assert + tick(); + + // We need to explicitly wait for the restore to finish + await restorePromise; + + let httpTestingController = TestBed.inject(HttpTestingController); + httpTestingController.verify(); + })); }) describe('backup', () => { it('Should upload a new backup file when there is no backup yet', async () => { // Arrange + let localStorageMock = getLocalStorageMock(); + when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); + let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) @@ -181,6 +221,9 @@ describe('DatabaseBackupAndRestoreService', () => { it('should overwrite the existing backup file when there is already an existing backup', async () => { // Arrange + let localStorageMock = getLocalStorageMock(); + when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); + let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index b2bbe8c..b5e8ced 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {exportDB} from "dexie-export-import"; import {db} from "./db"; import {FileUploadService} from "../file-upload/file-upload.service"; -import {filter, from, last, map, mergeMap, Observable, of, tap} from "rxjs"; +import {filter, finalize, from, last, map, mergeMap, Observable, of, tap} from "rxjs"; import {FileService} from "../file-list/file.service"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {HttpClient, HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; @@ -12,6 +12,8 @@ import {FilesCacheService} from "../files-cache/files-cache.service"; @Injectable() export class DatabaseBackupAndRestoreService { + + private static readonly LAST_DB_BACKUP_TIME = 'last_db_backup_time'; private static readonly DB_NAME = 'db.backup'; constructor(private fileUploadService: FileUploadService, private http: HttpClient, @@ -28,14 +30,17 @@ export class DatabaseBackupAndRestoreService { let dbFile = this.findExistingDbFile(); return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); }), - tap(httpEvent => this.backgroundTaskService.updateProgress(progress, httpEvent))); + tap(httpEvent => this.backgroundTaskService.updateProgress(progress, httpEvent)), + finalize(() => this.updateLastDbBackupTime())); } restore(): Observable { - let progress = this.backgroundTaskService.showProgress('Automatic restore', - "Downloading last backup", 2); let dbFile = this.findExistingDbFile(); - if (dbFile) { + let lastDbBackupTime = this.getLastDbBackupTime(); + let modifiedTime = dbFile?.modifiedTime ?? Date.now(); + if (dbFile && modifiedTime > lastDbBackupTime) { + let progress = this.backgroundTaskService.showProgress('Automatic restore', + "Downloading last backup", 2); let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + dbFile.id + '?alt=media'; return this.http.get(dlLink, {responseType: "blob", observe: "events", reportProgress: true}) .pipe( @@ -55,12 +60,27 @@ export class DatabaseBackupAndRestoreService { return from(db.import(dbDownloadResponse, {clearTablesBeforeImport: true})); }), tap(() => progress.next({index: 2, value: 100})), - map(() => void 0)); + map(() => void 0), + finalize(() => this.updateLastDbBackupTime())); } else { return of(); } } + private getLastDbBackupTime() { + let lastDbBackupTime = localStorage.getItem(DatabaseBackupAndRestoreService.LAST_DB_BACKUP_TIME); + if (lastDbBackupTime) { + return new Date(lastDbBackupTime); + } else { + // No backup so we return an arbitrary old value + return new Date(0); + } + } + + private updateLastDbBackupTime() { + localStorage.setItem(DatabaseBackupAndRestoreService.LAST_DB_BACKUP_TIME, new Date().toISOString()); + } + private findExistingDbFile() { let files = this.filesCacheService.getAll(); // TODO: ensure there cannot be any conflicts with user files diff --git a/src/app/file-list/file-list.component.html b/src/app/file-list/file-list.component.html index b83cd9f..0e1371c 100644 --- a/src/app/file-list/file-list.component.html +++ b/src/app/file-list/file-list.component.html @@ -52,7 +52,7 @@ Date - {{element.date | date:'medium'}} + {{ element.modifiedTime | date:'medium' }} diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 1569ec8..5647c8d 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -711,12 +711,13 @@ describe('FileListComponent', () => { }) }); -export function mockFileElement(name: string, parentId: string = 'baseFolderId', size: number = 0, date: string = ''): FileElement { +export function mockFileElement(name: string, parentId: string = 'baseFolderId', size: number = 0, date: string = '0'): FileElement { let id = name + '-' + uuid(); return { id: id, size: size, - date: date, + createdTime: new Date(date), + modifiedTime: new Date(date), name: name, iconLink: "link", dlLink: "dlLink", @@ -728,7 +729,8 @@ function mockFolderElement(name: string, parentId: string = 'baseFolderId'): Fol let id = name + '-' + uuid(); return { id: id, - date: '2023-08-02T14:54:55.556Z', + createdTime: new Date('2023-08-02T14:54:55.556Z'), + modifiedTime: new Date('2023-08-02T14:54:55.556Z'), name: name, iconLink: "link", parentId: parentId diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index b8044b0..cabb5e1 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -24,7 +24,8 @@ import {FilesCacheService} from "../files-cache/files-cache.service"; export interface FileOrFolderElement { id: string; name: string; - date: string; + createdTime: Date; + modifiedTime: Date; iconLink: string; parentId: string; } diff --git a/src/app/file-list/file.service.spec.ts b/src/app/file-list/file.service.spec.ts index 0332708..baa63ec 100644 --- a/src/app/file-list/file.service.spec.ts +++ b/src/app/file-list/file.service.spec.ts @@ -33,7 +33,7 @@ describe('FileService', () => { const req = httpTestingController.expectOne("https://www.googleapis.com/drive/v3/files?" + "q=trashed%20=%20false" + - "&fields=files(id,name,createdTime,size,iconLink,webContentLink,mimeType,parents)"); + "&fields=files(id,name,createdTime,modifiedTime,size,iconLink,webContentLink,mimeType,parents)"); expect(req.request.method).toEqual('GET'); req.flush({ "files": [ @@ -42,6 +42,7 @@ describe('FileService', () => { "size": "1811088", "name": "data.bin", "createdTime": "2023-08-14T14:48:44.928Z", + "modifiedTime": "2023-08-14T14:49:44.928Z", iconLink: "link", webContentLink: "dlLink", parents: ['pId12'] @@ -51,6 +52,7 @@ describe('FileService', () => { "size": "215142", "name": "document.pdf", "createdTime": "2023-08-14T12:28:46.935Z", + "modifiedTime": "2023-08-14T12:28:46.935Z", iconLink: "link", webContentLink: "dlLink", parents: ['pId12'] @@ -60,6 +62,7 @@ describe('FileService', () => { "size": "23207", "name": "test-render.png", "createdTime": "2023-08-03T14:54:55.556Z", + "modifiedTime": "2023-08-03T14:54:55.556Z", iconLink: "link", webContentLink: "dlLink", parents: ['pId3'] @@ -70,6 +73,7 @@ describe('FileService', () => { "name": "image", iconLink: "link", "createdTime": "2023-10-13T08:47:32.059Z", + "modifiedTime": "2023-10-13T08:47:32.059Z", parents: ['pId4'] }, { @@ -88,7 +92,8 @@ describe('FileService', () => { id: "id1", "size": 1811088, "name": "data.bin", - "date": "2023-08-14T14:48:44.928Z", + "createdTime": new Date("2023-08-14T14:48:44.928Z"), + "modifiedTime": new Date("2023-08-14T14:49:44.928Z"), iconLink: "link", dlLink: "dlLink", parentId: "pId12" @@ -97,7 +102,8 @@ describe('FileService', () => { id: "id2", "size": 215142, "name": "document.pdf", - "date": "2023-08-14T12:28:46.935Z", + "createdTime": new Date("2023-08-14T12:28:46.935Z"), + "modifiedTime": new Date("2023-08-14T12:28:46.935Z"), iconLink: "link", dlLink: "dlLink", parentId: "pId12" @@ -106,7 +112,8 @@ describe('FileService', () => { id: "id3", "size": 23207, "name": "test-render.png", - "date": "2023-08-03T14:54:55.556Z", + "createdTime": new Date("2023-08-03T14:54:55.556Z"), + "modifiedTime": new Date("2023-08-03T14:54:55.556Z"), iconLink: "link", dlLink: "dlLink", parentId: "pId3" @@ -114,7 +121,8 @@ describe('FileService', () => { { "id": "id4", "name": "image", - "date": "2023-10-13T08:47:32.059Z", + "createdTime": new Date("2023-10-13T08:47:32.059Z"), + "modifiedTime": new Date("2023-10-13T08:47:32.059Z"), iconLink: "link", parentId: "pId4" } as FolderElement diff --git a/src/app/file-list/file.service.ts b/src/app/file-list/file.service.ts index 2ee09cd..dcc7b5e 100644 --- a/src/app/file-list/file.service.ts +++ b/src/app/file-list/file.service.ts @@ -21,7 +21,7 @@ export class FileService { * Return all files managed by the app, except for the base folder */ findAll(): Observable { - const url = FileService.DRIVE_API_FILES_BASE_URL + '?q=' + encodeURI("trashed = false") + "&fields=" + encodeURI("files(id,name,createdTime,size,iconLink,webContentLink,mimeType,parents)"); + const url = FileService.DRIVE_API_FILES_BASE_URL + '?q=' + encodeURI("trashed = false") + "&fields=" + encodeURI("files(id,name,createdTime,modifiedTime,size,iconLink,webContentLink,mimeType,parents)"); return this.http.get(url).pipe(map(res => { if (res.files) { return res.files @@ -32,7 +32,8 @@ export class FileService { return { id: f.id, name: f.name, - date: f.createdTime, + createdTime: new Date(f.createdTime ?? '0'), + modifiedTime: new Date(f.modifiedTime ?? '0'), iconLink: f.iconLink, parentId: f.parents?.[0] } as FolderElement; @@ -40,7 +41,8 @@ export class FileService { return { id: f.id, name: f.name, - date: f.createdTime, + createdTime: new Date(f.createdTime ?? '0'), + modifiedTime: new Date(f.modifiedTime ?? '0'), size: Number(f.size), iconLink: f.iconLink, parentId: f.parents?.[0], diff --git a/src/app/file-upload/google-drive-auth.service.spec.ts b/src/app/file-upload/google-drive-auth.service.spec.ts index 218c10d..23ff164 100644 --- a/src/app/file-upload/google-drive-auth.service.spec.ts +++ b/src/app/file-upload/google-drive-auth.service.spec.ts @@ -2,17 +2,11 @@ import {GoogleDriveAuthService} from './google-drive-auth.service'; import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {It, mock, when} from 'strong-mock'; +import {Router} from "@angular/router"; +import {getLocalStorageMock,} from "../../testing/common-testing-function.spec"; import TokenClient = google.accounts.oauth2.TokenClient; import TokenResponse = google.accounts.oauth2.TokenResponse; -import {Router} from "@angular/router"; -function getLocalStorageMock() { - let localStorageMock = mock(); - Object.defineProperty(window, 'localStorage', { - value: localStorageMock - }); - return localStorageMock; -} function setupValidAuthenticationAndApiToken() { let localStorageMock = getLocalStorageMock(); diff --git a/src/app/files-cache/files-cache.service.ts b/src/app/files-cache/files-cache.service.ts index 8f2a26c..b365200 100644 --- a/src/app/files-cache/files-cache.service.ts +++ b/src/app/files-cache/files-cache.service.ts @@ -7,7 +7,6 @@ export class FilesCacheService { static reloadRouteData = false; constructor(private activatedRoute: ActivatedRoute, private router: Router) { - console.log(activatedRoute) } static shouldReloadRouteData() { diff --git a/src/testing/common-testing-function.spec.ts b/src/testing/common-testing-function.spec.ts index 5e3b2fd..a4b2396 100644 --- a/src/testing/common-testing-function.spec.ts +++ b/src/testing/common-testing-function.spec.ts @@ -4,6 +4,7 @@ import {MockRender, ngMocks} from "ng-mocks"; import {Router, RouterOutlet} from "@angular/router"; import {Location} from "@angular/common"; import {tick} from "@angular/core/testing"; +import {mock} from "strong-mock"; export async function findAsyncSequential( array: T[], @@ -90,3 +91,11 @@ export function navigateTo(path: string) { // Checking that we are on the right page. expect(location.path()).toEqual(path); } + +export function getLocalStorageMock() { + let localStorageMock = mock(); + Object.defineProperty(window, 'localStorage', { + value: localStorageMock + }); + return localStorageMock; +} From 19b6985f83fdfa7ce1d204fdc7e7923cc5cf44ce Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 18 Jan 2024 14:15:32 +0100 Subject: [PATCH 37/66] [us40] Support for including file content in rules --- ...atabase-backup-and-restore.service.spec.ts | 76 ++++++------- .../database-backup-and-restore.service.ts | 23 +--- src/app/file-list/file.service.spec.ts | 34 +++++- src/app/file-list/file.service.ts | 24 +++- src/app/rules/rule.service.spec.ts | 103 +++++++++++++++--- src/app/rules/rule.service.ts | 77 ++++++++----- 6 files changed, 224 insertions(+), 113 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 9b91627..153d0ff 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -7,10 +7,9 @@ import { getLocalStorageMock, mustBeConsumedAsyncObservable } from "../../testing/common-testing-function.spec"; -import {HttpClientModule, HttpEventType, HttpResponse} from "@angular/common/http"; +import {HttpEventType, HttpResponse} from "@angular/common/http"; import {mockFileElement} from "../file-list/file-list.component.spec"; -import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing"; -import {fakeAsync, TestBed, tick} from "@angular/core/testing"; +import {fakeAsync, tick} from "@angular/core/testing"; import {db} from "./db"; import {BehaviorSubject, lastValueFrom} from "rxjs"; import {mockBackgroundTaskService} from "../background-task/background-task.service.spec"; @@ -20,46 +19,43 @@ import {FileUploadService} from "../file-upload/file-upload.service"; import {FilesCacheService} from "../files-cache/files-cache.service"; import {FileElement} from "../file-list/file-list.component"; import {Rule} from "../rules/rule.repository"; +import {mockFileService} from "../file-list/file.service.spec"; -function setupMockForRestore() { +function setupMockForRestore(dbBackupFile: FileElement) { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); when(() => backgroundTaskService.showProgress("Automatic restore", "Downloading last backup", 2)) .thenReturn(progress); - when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); when(() => progress.next({index: 2, value: 0, description: "Importing last backup"})).thenReturn(); when(() => progress.next({index: 2, value: 100})).thenReturn(); -} -function mockLastBackupDownload(dbBackupFile: FileElement) { - let httpTestingController = TestBed.inject(HttpTestingController); - let request = httpTestingController.expectOne('https://www.googleapis.com/drive/v3/files/' + dbBackupFile.id + '?alt=media'); - request.flush(new Blob([JSON.stringify({ - "formatName": "dexie", - "formatVersion": 1, - "data": { - "databaseName": "StoreMyDocsDB", - "databaseVersion": 3, - "tables": [{"name": "rules", "schema": "++id", "rowCount": 1}], - "data": [{ - "tableName": "rules", - "inbound": true, - "rows": [{ - "name": "TestRule", - "category": ["Test1", "ChildTest1"], - "script": "return true", - "id": 1, - "$types": {"category": "arrayNonindexKeys"} + let fileService = mockFileService(); + when(() => fileService.downloadFile(dbBackupFile, progress)) + .thenReturn(mustBeConsumedAsyncObservable(new Blob([JSON.stringify({ + "formatName": "dexie", + "formatVersion": 1, + "data": { + "databaseName": "StoreMyDocsDB", + "databaseVersion": 3, + "tables": [{"name": "rules", "schema": "++id", "rowCount": 1}], + "data": [{ + "tableName": "rules", + "inbound": true, + "rows": [{ + "name": "TestRule", + "category": ["Test1", "ChildTest1"], + "script": "return true", + "id": 1, + "$types": {"category": "arrayNonindexKeys"} + }] }] - }] - } - })])); + } + })]))); } describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) - .replace(HttpClientModule, HttpClientTestingModule) .provide({ provide: FileUploadService, useValue: mock() @@ -86,14 +82,16 @@ describe('DatabaseBackupAndRestoreService', () => { describe('restore', () => { it('The database should be restored', fakeAsync(async () => { // Arrange + let localStorageMock = getLocalStorageMock(); when(() => localStorageMock.getItem('last_db_backup_time')).thenReturn(null); when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); - setupMockForRestore(); + let dbBackupFile = mockFileElement('db.backup'); + setupMockForRestore(dbBackupFile); + let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; - let dbBackupFile = mockFileElement('db.backup'); mockFilesCacheService([dbBackupFile]); // Act @@ -101,7 +99,6 @@ describe('DatabaseBackupAndRestoreService', () => { // Assert tick(); - mockLastBackupDownload(dbBackupFile); // We need to explicitly wait for the restore to finish await restorePromise; @@ -113,9 +110,6 @@ describe('DatabaseBackupAndRestoreService', () => { category: ['Test1', 'ChildTest1'], script: 'return true' }]); - - let httpTestingController = TestBed.inject(HttpTestingController); - httpTestingController.verify(); })); it('The database should be restored even if there is existing and conflicting old data', fakeAsync(async () => { @@ -123,11 +117,12 @@ describe('DatabaseBackupAndRestoreService', () => { let localStorageMock = getLocalStorageMock(); when(() => localStorageMock.getItem('last_db_backup_time')).thenReturn(null); when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); - setupMockForRestore(); + + let dbBackupFile = mockFileElement('db.backup'); + setupMockForRestore(dbBackupFile); let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; - let dbBackupFile = mockFileElement('db.backup'); mockFilesCacheService([dbBackupFile]); let oldRule: Rule = { id: 1, @@ -142,7 +137,6 @@ describe('DatabaseBackupAndRestoreService', () => { // Assert tick(); - mockLastBackupDownload(dbBackupFile); // We need to explicitly wait for the restore to finish await restorePromise; @@ -155,9 +149,6 @@ describe('DatabaseBackupAndRestoreService', () => { category: ['Test1', 'ChildTest1'], script: 'return true' }]); - - let httpTestingController = TestBed.inject(HttpTestingController); - httpTestingController.verify(); })); it('The database should not be restored if it is already up-to-date', fakeAsync(async () => { @@ -181,9 +172,6 @@ describe('DatabaseBackupAndRestoreService', () => { // We need to explicitly wait for the restore to finish await restorePromise; - - let httpTestingController = TestBed.inject(HttpTestingController); - httpTestingController.verify(); })); }) diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index b5e8ced..57a7d18 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -2,10 +2,9 @@ import {Injectable} from '@angular/core'; import {exportDB} from "dexie-export-import"; import {db} from "./db"; import {FileUploadService} from "../file-upload/file-upload.service"; -import {filter, finalize, from, last, map, mergeMap, Observable, of, tap} from "rxjs"; +import {finalize, from, map, mergeMap, Observable, of, tap} from "rxjs"; import {FileService} from "../file-list/file.service"; import {FileElement, isFileElement} from "../file-list/file-list.component"; -import {HttpClient, HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; import {BackgroundTaskService} from "../background-task/background-task.service"; import {FilesCacheService} from "../files-cache/files-cache.service"; @@ -16,9 +15,8 @@ export class DatabaseBackupAndRestoreService { private static readonly LAST_DB_BACKUP_TIME = 'last_db_backup_time'; private static readonly DB_NAME = 'db.backup'; - constructor(private fileUploadService: FileUploadService, private http: HttpClient, + constructor(private fileUploadService: FileUploadService, private fileService: FileService, private backgroundTaskService: BackgroundTaskService, private filesCacheService: FilesCacheService) { - // TODO: check refresh after restore } backup() { @@ -41,27 +39,16 @@ export class DatabaseBackupAndRestoreService { if (dbFile && modifiedTime > lastDbBackupTime) { let progress = this.backgroundTaskService.showProgress('Automatic restore', "Downloading last backup", 2); - let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + dbFile.id + '?alt=media'; - return this.http.get(dlLink, {responseType: "blob", observe: "events", reportProgress: true}) + return this.fileService.downloadFile(dbFile, progress) .pipe( - filter((e: HttpEvent): e is HttpProgressEvent | HttpResponse => - e.type === HttpEventType.DownloadProgress || e.type === HttpEventType.Response), - tap(event => this.backgroundTaskService.updateProgress(progress, event)), - last(), - mergeMap(event => { - if (event.type === HttpEventType.Response && event.body) { - return of(event.body); - } else { - return of(); - } - }), tap(() => progress.next({index: 2, value: 0, description: 'Importing last backup'})), mergeMap(dbDownloadResponse => { return from(db.import(dbDownloadResponse, {clearTablesBeforeImport: true})); }), tap(() => progress.next({index: 2, value: 100})), map(() => void 0), - finalize(() => this.updateLastDbBackupTime())); + finalize(() => this.updateLastDbBackupTime()) + ); } else { return of(); } diff --git a/src/app/file-list/file.service.spec.ts b/src/app/file-list/file.service.spec.ts index baa63ec..881f4bf 100644 --- a/src/app/file-list/file.service.spec.ts +++ b/src/app/file-list/file.service.spec.ts @@ -6,7 +6,9 @@ import {HttpClientTestingModule, HttpTestingController} from "@angular/common/ht import {fakeAsync, TestBed, tick} from "@angular/core/testing"; import {FileElement, FileOrFolderElement, FolderElement} from "./file-list.component"; import {mock, when} from "strong-mock"; -import {of} from "rxjs"; +import {BehaviorSubject, of} from "rxjs"; +import {mockFileElement} from "./file-list.component.spec"; +import {Progress} from "../background-task/background-task.service"; describe('FileService', () => { beforeEach(() => MockBuilder(FileService, AppModule) @@ -332,6 +334,33 @@ describe('FileService', () => { expect(result).toBe('folderId51'); })) }); + + describe("downloadFile", () => { + it('should download a file', async () => { + // Arrange + const service = MockRender(FileService).point.componentInstance; + + let fileElement = mockFileElement('file'); + + // Act + let result: Blob | undefined = undefined; + service.downloadFile(fileElement, mock>()) + .subscribe(value => result = value); + + // Assert + let httpTestingController = TestBed.inject(HttpTestingController); + let request = httpTestingController.expectOne('https://www.googleapis.com/drive/v3/files/' + fileElement.id + '?alt=media'); + request.flush(new Blob(['testContent'])); + + let textResult = ''; + if (result) { + textResult = await (result as Blob).text(); + } + expect(textResult).toBe('testContent'); + + httpTestingController.verify(); + }) + }) }); export function mockFileService() { @@ -342,7 +371,8 @@ export function mockFileService() { findAll: fileServiceMock.findAll, trash: fileServiceMock.trash, setCategory: fileServiceMock.setCategory, - findOrCreateBaseFolder: fileServiceMock.findOrCreateBaseFolder + findOrCreateBaseFolder: fileServiceMock.findOrCreateBaseFolder, + downloadFile: fileServiceMock.downloadFile } }); return fileServiceMock; diff --git a/src/app/file-list/file.service.ts b/src/app/file-list/file.service.ts index dcc7b5e..7b2079c 100644 --- a/src/app/file-list/file.service.ts +++ b/src/app/file-list/file.service.ts @@ -1,7 +1,8 @@ import {Injectable} from '@angular/core'; import {FileElement, FileOrFolderElement, FolderElement} from "./file-list.component"; -import {map, mergeMap, Observable, of} from "rxjs"; -import {HttpClient} from "@angular/common/http"; +import {BehaviorSubject, filter, last, map, mergeMap, Observable, of, tap} from "rxjs"; +import {HttpClient, HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; +import {BackgroundTaskService, Progress} from "../background-task/background-task.service"; @Injectable({ providedIn: 'root' @@ -10,7 +11,7 @@ export class FileService { static readonly BASE_FOLDER_NAME = 'storemydocs.ovh'; static readonly DRIVE_API_FILES_BASE_URL = 'https://www.googleapis.com/drive/v3/files'; - constructor(private http: HttpClient) { + constructor(private http: HttpClient, private backgroundTaskService: BackgroundTaskService) { } findOrCreateBaseFolder() { @@ -76,6 +77,23 @@ export class FileService { })) } + downloadFile(fileElement: FileElement, progress: BehaviorSubject): Observable { + let dlLink = FileService.DRIVE_API_FILES_BASE_URL + '/' + fileElement.id + '?alt=media'; + return this.http.get(dlLink, {responseType: "blob", observe: "events", reportProgress: true}) + .pipe( + filter((e: HttpEvent): e is HttpProgressEvent | HttpResponse => + e.type === HttpEventType.DownloadProgress || e.type === HttpEventType.Response), + tap(event => this.backgroundTaskService.updateProgress(progress, event)), + last(), + mergeMap(event => { + if (event.type === HttpEventType.Response && event.body) { + return of(event.body); + } else { + return of(); + } + })); + } + private findFolder(folderName: string, parentId: string | null) { let query = "trashed=false and mimeType='application/vnd.google-apps.folder' and name='" + folderName + "'"; if (parentId) { diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index cd33f04..f68666a 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -11,6 +11,10 @@ import {getSampleRules} from "./rules.component.spec"; import {RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; import {mockFilesCacheService} from "../files-cache/files-cache.service.spec"; +import {mockBackgroundTaskService} from "../background-task/background-task.service.spec"; +import {BehaviorSubject, lastValueFrom} from "rxjs"; +import {Progress} from "../background-task/background-task.service"; +import {FileElement} from "../file-list/file-list.component"; function mockBillCategoryFindOrCreate(fileService: FileService) { @@ -21,6 +25,38 @@ function mockBillCategoryFindOrCreate(fileService: FileService) { .thenReturn(mustBeConsumedAsyncObservable('billsCatId489')); } +function mockElectricityBillSample(file: FileElement, fileService: FileService) { + let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Running all rules", "", 3)) + .thenReturn(progress); + when(() => progress.next({ + index: 1, + value: 0, + description: "Downloading file content of 'electricity_bill.pdf'" + })).thenReturn() + when(() => progress.next({ + index: 2, + value: 0, + description: "Running rule 'Electric bill' for 'electricity_bill.pdf'" + })).thenReturn() + + mockBillCategoryFindOrCreate(fileService); + + when(() => fileService.downloadFile(file, progress)) + .thenReturn(mustBeConsumedAsyncObservable(new Blob(['not important content']))); + + const service = MockRender(RuleService).point.componentInstance; + + let ruleRepository = ngMocks.findInstance(RuleRepository); + when(() => ruleRepository.findAll()) + .thenResolve(getSampleRules()); + + mockFilesCacheService([file], true); + + return service; +} + describe('RuleService', () => { beforeEach(() => MockBuilder(RuleService, AppModule) .provide({ @@ -42,55 +78,90 @@ describe('RuleService', () => { }); describe('runAll', () => { - it('should automatically categorize a file', fakeAsync(() => { + it('should automatically categorize a file', fakeAsync(async () => { // Arrange + let file = mockFileElement('electricity_bill.pdf'); let fileService = mockFileService(); - mockBillCategoryFindOrCreate(fileService); // The file should be set to the bills category - let file = mockFileElement('electricity_bill.pdf'); when(() => fileService.setCategory(file.id, 'billsCatId489')) .thenReturn(mustBeConsumedAsyncObservable(undefined)); - const service = MockRender(RuleService).point.componentInstance; - - let ruleRepository = ngMocks.findInstance(RuleRepository); - when(() => ruleRepository.findAll()) - .thenResolve(getSampleRules()); - - mockFilesCacheService([file], true); + let service = mockElectricityBillSample(file, fileService); // Act - service.runAll().subscribe(); + let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); // Assert tick(); + await runAllPromise; // No failure in mock setup })); - it('should not categorize a file which is already in the correct category', fakeAsync(() => { + it('should not categorize a file which is already in the correct category', fakeAsync(async () => { // Arrange + let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); let fileService = mockFileService(); + let service = mockElectricityBillSample(file, fileService); + + // Act + let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); + // Assert + tick(); + await runAllPromise; + // No unexpected calls to fileService.setCategory + })); + + it('should automatically categorize a file (using txt file content)', fakeAsync(async () => { + // Arrange + let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + .thenReturn(progress); + when(() => progress.next({ + index: 1, + value: 0, + description: "Downloading file content of 'electricity_bill.txt'" + })).thenReturn() + when(() => progress.next({ + index: 2, + value: 0, + description: "Running rule 'Electric bill' for 'electricity_bill.txt'" + })).thenReturn() + let fileService = mockFileService(); mockBillCategoryFindOrCreate(fileService); + let file = mockFileElement('electricity_bill.txt'); + when(() => fileService.downloadFile(file, progress)) + .thenReturn(mustBeConsumedAsyncObservable(new Blob(['Electricity Bill. XXXXXX']))); + + // The file should be set to the bills category + when(() => fileService.setCategory(file.id, 'billsCatId489')) + .thenReturn(mustBeConsumedAsyncObservable(undefined)); const service = MockRender(RuleService).point.componentInstance; let ruleRepository = ngMocks.findInstance(RuleRepository); when(() => ruleRepository.findAll()) - .thenResolve(getSampleRules()); + .thenResolve([{ + name: 'Electric bill', + category: ['Electricity', 'Bills'], + script: 'return fileContent.startsWith("Electricity Bill");' + }]); - let file = mockFileElement('electricity_bill.pdf', 'billsCatId489'); mockFilesCacheService([file], true); // Act - service.runAll().subscribe(); + let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); // Assert tick(); - // No unexpected calls to fileService.setCategory + await runAllPromise; + // No failure in mock setup })); + // TODO: distinguish between binary file and readable files + // TODO: only keep one file content in memory and only if the file type content can be fetched }) }); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index d5b844e..701ce5c 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,29 +1,36 @@ import {Injectable} from '@angular/core'; import {FileService} from "../file-list/file.service"; -import {filter, from, map, mergeMap, Observable, of, zip} from "rxjs"; +import {BehaviorSubject, filter, from, last, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {Rule, RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; +import {BackgroundTaskService, Progress} from "../background-task/background-task.service"; +import {fromPromise} from "rxjs/internal/observable/innerFrom"; @Injectable() export class RuleService { - constructor(private fileService: FileService, private ruleRepository: RuleRepository, private filesCacheService: FilesCacheService) { + constructor(private fileService: FileService, private ruleRepository: RuleRepository, + private filesCacheService: FilesCacheService, private backgroundTaskService: BackgroundTaskService) { } runAll(): Observable { return from(this.ruleRepository.findAll()) .pipe(mergeMap(rules => { - let fileOrFolders = this.filesCacheService.getAll() - // Get all files - let files = fileOrFolders.filter(file => isFileElement(file)) - .map(value => value as FileElement); + let fileOrFolders = this.filesCacheService.getAll() + // Get all files + let files = fileOrFolders.filter(file => isFileElement(file)) + .map(value => value as FileElement); - // Run the script for each file to get the associated category - let fileToCategoryMap = this.computeFileToCategoryMap(files, rules); - - // Set the computed category for each files - return this.setAllFileCategory(fileToCategoryMap); - })); + // Run the script for each file to get the associated category + // The amount of step is one download per file and one per rule running for each file + let stepAmount = files.length * (1 + rules.length); + let progress = this.backgroundTaskService.showProgress('Running all rules', '', stepAmount); + return this.computeFileToCategoryMap(files, rules, progress) + }), + mergeMap(fileToCategoryMap => { + // Set the computed category for each files + return this.setAllFileCategory(fileToCategoryMap); + })); } create(rule: Rule) { @@ -41,30 +48,42 @@ export class RuleService { /** * Run the given rules on the given files and return the associated category for each file that got a matching rule */ - private computeFileToCategoryMap(files: FileElement[], rules: Rule[]) { + private computeFileToCategoryMap(files: FileElement[], rules: Rule[], progress: BehaviorSubject) { let fileToCategoryMap = new Map(); - files.forEach(file => { - // Find the first rule which matches - let rule = rules.find(rule => { - return this.run(rule, file); - }) - if (rule) { - fileToCategoryMap.set(file, rule.category); - } - }); + return from(files) + .pipe(mergeMap(file => { + progress.next({index: 1, value: 0, description: "Downloading file content of '" + file.name + "'"}); + return this.fileService.downloadFile(file, progress) + .pipe(mergeMap(blobContent => fromPromise(blobContent.text())), + map(fileContent => { + // Find the first rule which matches + let rule = rules.find(rule => { + progress.next({ + index: 2, + value: 0, + description: "Running rule '" + rule.name + "' for '" + file.name + "'" + }); + return this.run(rule, file, fileContent); + }) + if (rule) { + fileToCategoryMap.set(file, rule.category); + } + })) + }), + last(), + map(() => fileToCategoryMap)) - return fileToCategoryMap; } - private run(rule: Rule, file: FileElement) { - return Function("const fileName = arguments[0];" + rule.script)(file.name); + private run(rule: Rule, file: FileElement, fileContent: string) { + return Function("const fileName = arguments[0]; const fileContent = arguments[1]; " + rule.script)(file.name, fileContent); } /** * Find or create the categories for each file and associate them */ - private setAllFileCategory(fileToCategoryMap: Map) { + private setAllFileCategory(fileToCategoryMap: Map): Observable { let baseFolderId = this.filesCacheService.getBaseFolder(); let categoryRequests: Observable[] = []; fileToCategoryMap @@ -76,10 +95,8 @@ export class RuleService { return this.fileService.setCategory(file.id, categoryId); }))); }); - let observable = zip(categoryRequests); - return observable - .pipe(map(() => { - })); + return zip(categoryRequests).pipe(map(() => { + })); } // TODO: move and refactor duplicate to FileService From e0dd04d892f0b81e68716f1f8be45270a0e97e08 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 18 Jan 2024 14:40:18 +0100 Subject: [PATCH 38/66] [us40] Fix steps indexes (of showProgress) when running rules --- src/app/rules/rule.service.spec.ts | 32 +++++++++++++++++-- src/app/rules/rule.service.ts | 49 ++++++++++++++++-------------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index f68666a..c934a1b 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -117,18 +117,35 @@ describe('RuleService', () => { // Arrange let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + when(() => backgroundTaskService.showProgress("Running all rules", "", 6)) .thenReturn(progress); when(() => progress.next({ index: 1, value: 0, description: "Downloading file content of 'electricity_bill.txt'" - })).thenReturn() + })).thenReturn(); when(() => progress.next({ index: 2, value: 0, description: "Running rule 'Electric bill' for 'electricity_bill.txt'" + })).thenReturn(); + + when(() => progress.next({ + index: 4, + value: 0, + description: "Downloading file content of 'something_else.txt'" + })).thenReturn(); + when(() => progress.next({ + index: 5, + value: 0, + description: "Running rule 'Electric bill' for 'something_else.txt'" + })).thenReturn(); + when(() => progress.next({ + index: 6, + value: 0, + description: "Running rule 'Dumb rule' for 'something_else.txt'" })).thenReturn() + let fileService = mockFileService(); mockBillCategoryFindOrCreate(fileService); @@ -136,6 +153,10 @@ describe('RuleService', () => { when(() => fileService.downloadFile(file, progress)) .thenReturn(mustBeConsumedAsyncObservable(new Blob(['Electricity Bill. XXXXXX']))); + let otherFile = mockFileElement('something_else.txt'); + when(() => fileService.downloadFile(otherFile, progress)) + .thenReturn(mustBeConsumedAsyncObservable(new Blob(['Something else']))); + // The file should be set to the bills category when(() => fileService.setCategory(file.id, 'billsCatId489')) .thenReturn(mustBeConsumedAsyncObservable(undefined)); @@ -148,9 +169,13 @@ describe('RuleService', () => { name: 'Electric bill', category: ['Electricity', 'Bills'], script: 'return fileContent.startsWith("Electricity Bill");' + }, { + name: 'Dumb rule', + category: ['Dumb'], + script: 'return false' }]); - mockFilesCacheService([file], true); + mockFilesCacheService([file, otherFile], true); // Act let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); @@ -160,6 +185,7 @@ describe('RuleService', () => { await runAllPromise; // No failure in mock setup })); + // TODO: handle errors on file download? // TODO: distinguish between binary file and readable files // TODO: only keep one file content in memory and only if the file type content can be fetched }) diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 701ce5c..021b1b3 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,6 +1,6 @@ import {Injectable} from '@angular/core'; import {FileService} from "../file-list/file.service"; -import {BehaviorSubject, filter, from, last, map, mergeMap, Observable, of, zip} from "rxjs"; +import {BehaviorSubject, filter, from, map, mergeMap, Observable, of, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {Rule, RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; @@ -51,28 +51,31 @@ export class RuleService { private computeFileToCategoryMap(files: FileElement[], rules: Rule[], progress: BehaviorSubject) { let fileToCategoryMap = new Map(); - return from(files) - .pipe(mergeMap(file => { - progress.next({index: 1, value: 0, description: "Downloading file content of '" + file.name + "'"}); - return this.fileService.downloadFile(file, progress) - .pipe(mergeMap(blobContent => fromPromise(blobContent.text())), - map(fileContent => { - // Find the first rule which matches - let rule = rules.find(rule => { - progress.next({ - index: 2, - value: 0, - description: "Running rule '" + rule.name + "' for '" + file.name + "'" - }); - return this.run(rule, file, fileContent); - }) - if (rule) { - fileToCategoryMap.set(file, rule.category); - } - })) - }), - last(), - map(() => fileToCategoryMap)) + return zip(files.map((file, fileIndex) => { + let progressIndex = 1 + fileIndex * (rules.length + 1); + progress.next({ + index: progressIndex, + value: 0, + description: "Downloading file content of '" + file.name + "'" + }); + return this.fileService.downloadFile(file, progress) + .pipe(mergeMap(blobContent => fromPromise(blobContent.text())), + map(fileContent => { + // Find the first rule which matches + let rule = rules.find((rule, ruleIndex) => { + progress.next({ + index: progressIndex + 1 + ruleIndex, + value: 0, + description: "Running rule '" + rule.name + "' for '" + file.name + "'" + }); + return this.run(rule, file, fileContent); + }) + if (rule) { + fileToCategoryMap.set(file, rule.category); + } + })) + })) + .pipe(map(() => fileToCategoryMap)); } From 36a2f963312257debfbd29495cddd55a1a2dc71a Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 18 Jan 2024 15:36:56 +0100 Subject: [PATCH 39/66] [us40] Download file content only for simple text file when running rules --- src/app/file-list/file-list.component.spec.ts | 1 + src/app/file-list/file-list.component.ts | 1 + src/app/file-list/file.service.spec.ts | 6 ++ src/app/file-list/file.service.ts | 29 ++++--- src/app/rules/rule.service.spec.ts | 52 +++++++++++- src/app/rules/rule.service.ts | 83 +++++++++++-------- 6 files changed, 121 insertions(+), 51 deletions(-) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 5647c8d..3ddb696 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -714,6 +714,7 @@ describe('FileListComponent', () => { export function mockFileElement(name: string, parentId: string = 'baseFolderId', size: number = 0, date: string = '0'): FileElement { let id = name + '-' + uuid(); return { + mimeType: "text/plain", id: id, size: size, createdTime: new Date(date), diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index cabb5e1..1e9f5a3 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -31,6 +31,7 @@ export interface FileOrFolderElement { } export interface FileElement extends FileOrFolderElement { + mimeType: string; size: number; dlLink: string; } diff --git a/src/app/file-list/file.service.spec.ts b/src/app/file-list/file.service.spec.ts index 881f4bf..f65810e 100644 --- a/src/app/file-list/file.service.spec.ts +++ b/src/app/file-list/file.service.spec.ts @@ -43,6 +43,7 @@ describe('FileService', () => { id: "id1", "size": "1811088", "name": "data.bin", + "mimeType": "application/octet-stream", "createdTime": "2023-08-14T14:48:44.928Z", "modifiedTime": "2023-08-14T14:49:44.928Z", iconLink: "link", @@ -53,6 +54,7 @@ describe('FileService', () => { id: "id2", "size": "215142", "name": "document.pdf", + "mimeType": "application/pdf", "createdTime": "2023-08-14T12:28:46.935Z", "modifiedTime": "2023-08-14T12:28:46.935Z", iconLink: "link", @@ -63,6 +65,7 @@ describe('FileService', () => { id: "id3", "size": "23207", "name": "test-render.png", + "mimeType": "image/png", "createdTime": "2023-08-03T14:54:55.556Z", "modifiedTime": "2023-08-03T14:54:55.556Z", iconLink: "link", @@ -94,6 +97,7 @@ describe('FileService', () => { id: "id1", "size": 1811088, "name": "data.bin", + "mimeType": "application/octet-stream", "createdTime": new Date("2023-08-14T14:48:44.928Z"), "modifiedTime": new Date("2023-08-14T14:49:44.928Z"), iconLink: "link", @@ -104,6 +108,7 @@ describe('FileService', () => { id: "id2", "size": 215142, "name": "document.pdf", + "mimeType": "application/pdf", "createdTime": new Date("2023-08-14T12:28:46.935Z"), "modifiedTime": new Date("2023-08-14T12:28:46.935Z"), iconLink: "link", @@ -114,6 +119,7 @@ describe('FileService', () => { id: "id3", "size": 23207, "name": "test-render.png", + "mimeType": "image/png", "createdTime": new Date("2023-08-03T14:54:55.556Z"), "modifiedTime": new Date("2023-08-03T14:54:55.556Z"), iconLink: "link", diff --git a/src/app/file-list/file.service.ts b/src/app/file-list/file.service.ts index 7b2079c..5bd5721 100644 --- a/src/app/file-list/file.service.ts +++ b/src/app/file-list/file.service.ts @@ -30,25 +30,28 @@ export class FileService { .filter(f => f.name !== FileService.BASE_FOLDER_NAME) .map(f => { if (f.mimeType == 'application/vnd.google-apps.folder') { - return { - id: f.id, - name: f.name, + let folderElement: FolderElement = { + id: f.id ?? '', + name: f.name ?? '', createdTime: new Date(f.createdTime ?? '0'), modifiedTime: new Date(f.modifiedTime ?? '0'), - iconLink: f.iconLink, - parentId: f.parents?.[0] - } as FolderElement; + iconLink: f.iconLink ?? '', + parentId: f.parents?.[0] ?? '' + }; + return folderElement; } else { - return { - id: f.id, - name: f.name, + let fileElement: FileElement = { + id: f.id ?? '', + name: f.name ?? '', createdTime: new Date(f.createdTime ?? '0'), modifiedTime: new Date(f.modifiedTime ?? '0'), size: Number(f.size), - iconLink: f.iconLink, - parentId: f.parents?.[0], - dlLink: f.webContentLink - } as FileElement; + iconLink: f.iconLink ?? '', + parentId: f.parents?.[0] ?? '', + dlLink: f.webContentLink ?? '', + mimeType: f.mimeType ?? '' + }; + return fileElement; } }) } else { diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index c934a1b..74c7e1e 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -40,6 +40,10 @@ function mockElectricityBillSample(file: FileElement, fileService: FileService) value: 0, description: "Running rule 'Electric bill' for 'electricity_bill.pdf'" })).thenReturn() + when(() => progress.next({ + index: 3, + value: 100, + })).thenReturn(); mockBillCategoryFindOrCreate(fileService); @@ -145,6 +149,10 @@ describe('RuleService', () => { value: 0, description: "Running rule 'Dumb rule' for 'something_else.txt'" })).thenReturn() + when(() => progress.next({ + index: 6, + value: 100, + })).thenReturn(); let fileService = mockFileService(); mockBillCategoryFindOrCreate(fileService); @@ -185,8 +193,48 @@ describe('RuleService', () => { await runAllPromise; // No failure in mock setup })); - // TODO: handle errors on file download? - // TODO: distinguish between binary file and readable files + + it('should not download content of a binary file', fakeAsync(async () => { + // Arrange + let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + .thenReturn(progress); + when(() => progress.next({ + index: 2, + value: 0, + description: "Running rule 'Dumb file content rule' for 'test.png'" + })).thenReturn(); + when(() => progress.next({ + index: 2, + value: 100, + })).thenReturn(); + + mockFileService(); + + let file = mockFileElement('test.png'); + file.mimeType = 'image/png' + + const service = MockRender(RuleService).point.componentInstance; + + let ruleRepository = ngMocks.findInstance(RuleRepository); + when(() => ruleRepository.findAll()) + .thenResolve([{ + name: 'Dumb file content rule', + category: ['Dumb'], + script: 'return fileContent.includes("test")' + }]); + + mockFilesCacheService([file], true); + + // Act + let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); + + // Assert + tick(); + await runAllPromise; + // No failure in mock setup + })); // TODO: only keep one file content in memory and only if the file type content can be fetched }) }); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 021b1b3..50dd984 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,6 +1,6 @@ import {Injectable} from '@angular/core'; import {FileService} from "../file-list/file.service"; -import {BehaviorSubject, filter, from, map, mergeMap, Observable, of, zip} from "rxjs"; +import {BehaviorSubject, filter, from, map, mergeMap, Observable, of, tap, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {Rule, RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; @@ -16,21 +16,21 @@ export class RuleService { runAll(): Observable { return from(this.ruleRepository.findAll()) .pipe(mergeMap(rules => { - let fileOrFolders = this.filesCacheService.getAll() - // Get all files - let files = fileOrFolders.filter(file => isFileElement(file)) - .map(value => value as FileElement); + let fileOrFolders = this.filesCacheService.getAll() + // Get all files + let files = fileOrFolders.filter(file => isFileElement(file)) + .map(value => value as FileElement); - // Run the script for each file to get the associated category - // The amount of step is one download per file and one per rule running for each file - let stepAmount = files.length * (1 + rules.length); - let progress = this.backgroundTaskService.showProgress('Running all rules', '', stepAmount); - return this.computeFileToCategoryMap(files, rules, progress) - }), - mergeMap(fileToCategoryMap => { - // Set the computed category for each files - return this.setAllFileCategory(fileToCategoryMap); - })); + // Run the script for each file to get the associated category + // The amount of step is one download per file and one per rule running for each file + let stepAmount = files.length * (1 + rules.length); + let progress = this.backgroundTaskService.showProgress('Running all rules', '', stepAmount); + return this.computeFileToCategoryMap(files, rules, progress) + .pipe(mergeMap(fileToCategoryMap => { + // Set the computed category for each files + return this.setAllFileCategory(fileToCategoryMap); + }), tap({complete: () => progress.next({value: 100, index: stepAmount})})) + })); } create(rule: Rule) { @@ -53,32 +53,43 @@ export class RuleService { return zip(files.map((file, fileIndex) => { let progressIndex = 1 + fileIndex * (rules.length + 1); - progress.next({ - index: progressIndex, - value: 0, - description: "Downloading file content of '" + file.name + "'" - }); - return this.fileService.downloadFile(file, progress) - .pipe(mergeMap(blobContent => fromPromise(blobContent.text())), - map(fileContent => { - // Find the first rule which matches - let rule = rules.find((rule, ruleIndex) => { - progress.next({ - index: progressIndex + 1 + ruleIndex, - value: 0, - description: "Running rule '" + rule.name + "' for '" + file.name + "'" - }); - return this.run(rule, file, fileContent); - }) - if (rule) { - fileToCategoryMap.set(file, rule.category); - } - })) + + let fileContentObservable: Observable; + if (this.isFileContentReadable(file)) { + progress.next({ + index: progressIndex, + value: 0, + description: "Downloading file content of '" + file.name + "'" + }); + fileContentObservable = this.fileService.downloadFile(file, progress) + .pipe(mergeMap(blobContent => fromPromise(blobContent.text()))); + } else { + fileContentObservable = of(""); + } + return fileContentObservable.pipe( + map(fileContent => { + // Find the first rule which matches + let rule = rules.find((rule, ruleIndex) => { + progress.next({ + index: progressIndex + 1 + ruleIndex, + value: 0, + description: "Running rule '" + rule.name + "' for '" + file.name + "'" + }); + return this.run(rule, file, fileContent); + }) + if (rule) { + fileToCategoryMap.set(file, rule.category); + } + })) })) .pipe(map(() => fileToCategoryMap)); } + private isFileContentReadable(file: FileElement) { + return file.mimeType.startsWith('text/'); + } + private run(rule: Rule, file: FileElement, fileContent: string) { return Function("const fileName = arguments[0]; const fileContent = arguments[1]; " + rule.script)(file.name, fileContent); } From 6bf9defebf3926399f9585a808a12215b793f8ac Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 19 Jan 2024 13:37:18 +0100 Subject: [PATCH 40/66] [us40] Fix UI update and message orders while using web worker for async background running of rule --- angular.json | 6 +- src/app/rules/rule.service.ts | 107 +++++++++++++++++++++++----------- src/app/rules/rule.worker.ts | 16 +++++ tsconfig.app.json | 1 + tsconfig.spec.json | 3 + tsconfig.worker.json | 15 +++++ 6 files changed, 111 insertions(+), 37 deletions(-) create mode 100644 src/app/rules/rule.worker.ts create mode 100644 tsconfig.worker.json diff --git a/angular.json b/angular.json index 09bec4b..67a250c 100644 --- a/angular.json +++ b/angular.json @@ -33,7 +33,8 @@ "src/theme.scss", "src/styles.scss" ], - "scripts": [] + "scripts": [], + "webWorkerTsConfig": "tsconfig.worker.json" }, "configurations": { "production": { @@ -99,7 +100,8 @@ "src/styles.scss" ], "scripts": [], - "karmaConfig": "karma.conf.js" + "karmaConfig": "karma.conf.js", + "webWorkerTsConfig": "tsconfig.worker.json" } } } diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 50dd984..6059bd5 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,16 +1,35 @@ import {Injectable} from '@angular/core'; import {FileService} from "../file-list/file.service"; -import {BehaviorSubject, filter, from, map, mergeMap, Observable, of, tap, zip} from "rxjs"; +import {BehaviorSubject, concatMap, filter, find, from, last, map, mergeMap, Observable, of, tap, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {Rule, RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; import {BackgroundTaskService, Progress} from "../background-task/background-task.service"; import {fromPromise} from "rxjs/internal/observable/innerFrom"; +export interface RuleResult { + rule: Rule, + value: boolean +} + +export interface RuleWorkerParams { + script: string, + fileName: string, + fileContent: string, +} + +interface WorkerResponse { + data: boolean; +} + @Injectable() export class RuleService { + private worker: Worker; + constructor(private fileService: FileService, private ruleRepository: RuleRepository, private filesCacheService: FilesCacheService, private backgroundTaskService: BackgroundTaskService) { + // Create a new + this.worker = new Worker(new URL('./rule.worker', import.meta.url)); } runAll(): Observable { @@ -50,39 +69,46 @@ export class RuleService { */ private computeFileToCategoryMap(files: FileElement[], rules: Rule[], progress: BehaviorSubject) { let fileToCategoryMap = new Map(); + return zip(from(files) + .pipe(concatMap((file, fileIndex) => { + let progressIndex = 1 + fileIndex * (rules.length + 1); - return zip(files.map((file, fileIndex) => { - let progressIndex = 1 + fileIndex * (rules.length + 1); - - let fileContentObservable: Observable; - if (this.isFileContentReadable(file)) { - progress.next({ - index: progressIndex, - value: 0, - description: "Downloading file content of '" + file.name + "'" - }); - fileContentObservable = this.fileService.downloadFile(file, progress) - .pipe(mergeMap(blobContent => fromPromise(blobContent.text()))); - } else { - fileContentObservable = of(""); - } - return fileContentObservable.pipe( - map(fileContent => { - // Find the first rule which matches - let rule = rules.find((rule, ruleIndex) => { - progress.next({ - index: progressIndex + 1 + ruleIndex, - value: 0, - description: "Running rule '" + rule.name + "' for '" + file.name + "'" - }); - return this.run(rule, file, fileContent); - }) - if (rule) { - fileToCategoryMap.set(file, rule.category); - } - })) - })) - .pipe(map(() => fileToCategoryMap)); + let fileContentObservable: Observable; + if (this.isFileContentReadable(file)) { + progress.next({ + index: progressIndex, + value: 0, + description: "Downloading file content of '" + file.name + "'" + }); + fileContentObservable = this.fileService.downloadFile(file, progress) + .pipe(mergeMap(blobContent => fromPromise(blobContent.text()))); + } else { + fileContentObservable = of(""); + } + return fileContentObservable.pipe( + mergeMap(fileContent => { + // Find the first rule which matches + return from(rules).pipe(concatMap((rule, ruleIndex) => { + progress.next({ + index: progressIndex + 1 + ruleIndex, + value: 0, + description: "Running rule '" + rule.name + "' for '" + file.name + "'" + }); + return this.run(rule, file, fileContent, progress, progressIndex + 1 + ruleIndex); + }), + // Find will stop running further scripts once we got a match + find(result => { + return result.value; + }), + map(result => { + if (result) { + fileToCategoryMap.set(file, result.rule.category); + } + })); + })); + }))) + .pipe(last(), + map(() => fileToCategoryMap)); } @@ -90,8 +116,19 @@ export class RuleService { return file.mimeType.startsWith('text/'); } - private run(rule: Rule, file: FileElement, fileContent: string) { - return Function("const fileName = arguments[0]; const fileContent = arguments[1]; " + rule.script)(file.name, fileContent); + private run(rule: Rule, file: FileElement, fileContent: string, progress: BehaviorSubject, progressIndex: number): Observable { + return new Observable(subscriber => { + this.worker.onmessage = ({data}: WorkerResponse) => { + subscriber.next({rule: rule, value: data}); + subscriber.complete(); + }; + let params: RuleWorkerParams = { + script: rule.script, + fileName: file.name, + fileContent: fileContent + }; + this.worker.postMessage(params); + }); } /** diff --git a/src/app/rules/rule.worker.ts b/src/app/rules/rule.worker.ts new file mode 100644 index 0000000..ce7db3c --- /dev/null +++ b/src/app/rules/rule.worker.ts @@ -0,0 +1,16 @@ +/// + +interface RuleWorkerParams { + script: string, + fileName: string, + fileContent: string, +} + +interface Data { + data: RuleWorkerParams +} + +addEventListener('message', ({data}: Data) => { + let result = Function("const fileName = arguments[0]; const fileContent = arguments[1]; " + data.script)(data.fileName, data.fileContent); + postMessage(result); +}); diff --git a/tsconfig.app.json b/tsconfig.app.json index a159e85..2aa90be 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -18,6 +18,7 @@ ], "exclude": [ "src/**/*.spec.ts", + "src/**/*.worker.ts", "src/test.ts" ] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 6fa12d3..c335bbd 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -12,5 +12,8 @@ }, "include": [ "src/**/*.ts" + ], + "exclude": [ + "src/**/*.worker.ts" ] } diff --git a/tsconfig.worker.json b/tsconfig.worker.json new file mode 100644 index 0000000..22dc454 --- /dev/null +++ b/tsconfig.worker.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/worker", + "lib": [ + "es2018", + "webworker" + ], + "types": [] + }, + "include": [ + "src/**/*.worker.ts" + ] +} From d69fd7be1a693972c1557b352b2f4f57a5d800d2 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 25 Jan 2024 10:51:02 +0100 Subject: [PATCH 41/66] [us40] Update all dependencies since this may solve an incompatibility with pdfjslib install --- angular.json | 6 +- package-lock.json | 6005 +++++++++++++++++---------------------------- package.json | 36 +- 3 files changed, 2324 insertions(+), 3723 deletions(-) diff --git a/angular.json b/angular.json index 67a250c..70b15bf 100644 --- a/angular.json +++ b/angular.json @@ -67,10 +67,10 @@ "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "browserTarget": "storemydocs:build:production" + "buildTarget": "storemydocs:build:production" }, "development": { - "browserTarget": "storemydocs:build:development" + "buildTarget": "storemydocs:build:development" } }, "defaultConfiguration": "development" @@ -78,7 +78,7 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "storemydocs:build" + "buildTarget": "storemydocs:build" } }, "test": { diff --git a/package-lock.json b/package-lock.json index 61f4d33..aa0f64f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,16 @@ "name": "storemydocs", "version": "0.3.1-12-g5d0e863", "dependencies": { - "@angular/animations": "^16.1.0", - "@angular/cdk": "^16.1.4", - "@angular/common": "^16.1.0", - "@angular/compiler": "^16.1.0", - "@angular/core": "^16.1.0", - "@angular/forms": "^16.2.12", - "@angular/material": "^16.2.12", - "@angular/platform-browser": "^16.1.0", - "@angular/platform-browser-dynamic": "^16.2.12", - "@angular/router": "^16.2.12", + "@angular/animations": "^17.1.1", + "@angular/cdk": "^17.1.0", + "@angular/common": "^17.1.1", + "@angular/compiler": "^17.1.1", + "@angular/core": "^17.1.1", + "@angular/forms": "^17.1.1", + "@angular/material": "^17.1.0", + "@angular/platform-browser": "^17.1.1", + "@angular/platform-browser-dynamic": "^17.1.1", + "@angular/router": "^17.1.1", "@auth0/angular-jwt": "^5.2.0", "dexie": "^3.2.4", "dexie-export-import": "^4.0.7", @@ -25,16 +25,16 @@ "ngx-filesize": "^3.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", - "zone.js": "^0.13.3" + "zone.js": "^0.14.3" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.2.10", - "@angular/cli": "~16.1.3", - "@angular/compiler-cli": "^16.2.12", + "@angular-devkit/build-angular": "^17.1.1", + "@angular/cli": "~17.1.1", + "@angular/compiler-cli": "^17.1.1", "@playwright/test": "^1.39.0", - "@stryker-mutator/core": "^7.3.0", - "@stryker-mutator/karma-runner": "^7.3.0", - "@stryker-mutator/typescript-checker": "^7.3.0", + "@stryker-mutator/core": "^8.0.0", + "@stryker-mutator/karma-runner": "^8.0.0", + "@stryker-mutator/typescript-checker": "^8.0.0", "@types/gapi.client.drive-v3": "^0.0.4", "@types/google.accounts": "^0.0.14", "@types/google.picker": "^0.0.42", @@ -50,7 +50,7 @@ "ng-mocks": "^14.11.0", "strong-mock": "^8.0.1", "stryker-cli": "^1.0.2", - "typescript": "~5.1.6" + "typescript": "~5.3.3" } }, "node_modules/@ampproject/remapping": { @@ -67,111 +67,113 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1602.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.10.tgz", - "integrity": "sha512-FwemQXh3edqA/S6zPpsqKei5v7gt0R0WpjJoAJaz+FOpfDwij1fwnKr88XINY8xcefTcQaTDQxJZheJShA/hHw==", + "version": "0.1701.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1701.1.tgz", + "integrity": "sha512-vT3ZRAIfNyIg0vJWT6umPbCKiKFCukNkxLe9kgOU0tinZKNr/LgWYaBZ92Rxxi6C3NrAnmAYjsih8x4zdyoRXw==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.10", + "@angular-devkit/core": "17.1.1", "rxjs": "7.8.1" }, "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.10.tgz", - "integrity": "sha512-msB/qjIsAOySDxdU5DpcX2sWGUEJOFIO03O9+HbtLwf3NDfe74mFfejxuKlHJXIJdgpM2Zc948M6+618QKpUYA==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.1.1.tgz", + "integrity": "sha512-GchDM8H+RQNts731c+jnhDgOm0PnCS3YB12uVwRiGsaNsUMrqKnu3P0poh6AImDMPyXKnIvTWLDCMD8TDziR0A==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1602.10", - "@angular-devkit/build-webpack": "0.1602.10", - "@angular-devkit/core": "16.2.10", - "@babel/core": "7.22.9", - "@babel/generator": "7.22.9", + "@angular-devkit/architect": "0.1701.1", + "@angular-devkit/build-webpack": "0.1701.1", + "@angular-devkit/core": "17.1.1", + "@babel/core": "7.23.7", + "@babel/generator": "7.23.6", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-proposal-async-generator-functions": "7.20.7", - "@babel/plugin-transform-async-to-generator": "7.22.5", - "@babel/plugin-transform-runtime": "7.22.9", - "@babel/preset-env": "7.22.9", - "@babel/runtime": "7.22.6", - "@babel/template": "7.22.5", + "@babel/plugin-transform-async-generator-functions": "7.23.7", + "@babel/plugin-transform-async-to-generator": "7.23.3", + "@babel/plugin-transform-runtime": "7.23.7", + "@babel/preset-env": "7.23.7", + "@babel/runtime": "7.23.7", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.2.10", - "@vitejs/plugin-basic-ssl": "1.0.1", + "@ngtools/webpack": "17.1.1", + "@vitejs/plugin-basic-ssl": "1.0.2", "ansi-colors": "4.1.3", - "autoprefixer": "10.4.14", + "autoprefixer": "10.4.16", "babel-loader": "9.1.3", "babel-plugin-istanbul": "6.1.1", "browserslist": "^4.21.5", - "chokidar": "3.5.3", "copy-webpack-plugin": "11.0.0", "critters": "0.0.20", "css-loader": "6.8.1", - "esbuild-wasm": "0.18.17", - "fast-glob": "3.3.1", - "guess-parser": "0.4.22", - "https-proxy-agent": "5.0.1", - "inquirer": "8.2.4", + "esbuild-wasm": "0.19.11", + "fast-glob": "3.3.2", + "http-proxy-middleware": "2.0.6", + "https-proxy-agent": "7.0.2", + "inquirer": "9.2.12", "jsonc-parser": "3.2.0", "karma-source-map-support": "1.4.0", - "less": "4.1.3", + "less": "4.2.0", "less-loader": "11.1.0", "license-webpack-plugin": "4.0.2", "loader-utils": "3.2.1", - "magic-string": "0.30.1", + "magic-string": "0.30.5", "mini-css-extract-plugin": "2.7.6", - "mrmime": "1.0.1", + "mrmime": "2.0.0", "open": "8.4.2", "ora": "5.4.1", "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "2.3.1", - "piscina": "4.0.0", - "postcss": "8.4.31", - "postcss-loader": "7.3.3", + "picomatch": "3.0.1", + "piscina": "4.2.1", + "postcss": "8.4.33", + "postcss-loader": "7.3.4", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.64.1", - "sass-loader": "13.3.2", + "sass": "1.69.7", + "sass-loader": "13.3.3", "semver": "7.5.4", - "source-map-loader": "4.0.1", + "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.19.2", + "terser": "5.26.0", "text-table": "0.2.0", "tree-kill": "1.2.2", - "tslib": "2.6.1", - "vite": "4.4.7", - "webpack": "5.88.2", + "tslib": "2.6.2", + "undici": "6.2.1", + "vite": "5.0.12", + "watchpack": "2.4.0", + "webpack": "5.89.0", "webpack-dev-middleware": "6.1.1", "webpack-dev-server": "4.15.1", - "webpack-merge": "5.9.0", + "webpack-merge": "5.10.0", "webpack-subresource-integrity": "5.1.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.18.17" + "esbuild": "0.19.11" }, "peerDependencies": { - "@angular/compiler-cli": "^16.0.0", - "@angular/localize": "^16.0.0", - "@angular/platform-server": "^16.0.0", - "@angular/service-worker": "^16.0.0", + "@angular/compiler-cli": "^17.0.0", + "@angular/localize": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/service-worker": "^17.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", - "ng-packagr": "^16.0.0", + "ng-packagr": "^17.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=4.9.3 <5.2" + "typescript": ">=5.2 <5.4" }, "peerDependenciesMeta": { "@angular/localize": { @@ -183,6 +185,12 @@ "@angular/service-worker": { "optional": true }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, "jest": { "optional": true }, @@ -203,23 +211,17 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", - "dev": true - }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1602.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.10.tgz", - "integrity": "sha512-H7HiFKbZl/xVxpr1RH05SGawTpA1417wvr2nFGRu2OiePd0lPr6pIhcq8F8gt7JcA8yZKKaqjn2gU+6um2MFLg==", + "version": "0.1701.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1701.1.tgz", + "integrity": "sha512-YgNl/6xLmI0XdUCu/H4Jyi34BhrANCDP4N2Pz+tGwnz2+Vl8oZGLPGtKVbh/LKSAmEHk/B6GQUekSBpKWrPJoA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.10", + "@angular-devkit/architect": "0.1701.1", "rxjs": "7.8.1" }, "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -229,20 +231,20 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.10.tgz", - "integrity": "sha512-eo7suLDjyu5bSlEr4TluYkFm4v2PVLSAPgnau8XHHlN5Yg4P/BZ00ve7LA7C9S1gzRSCrxQhK5ki4rnoFTo5zg==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.1.tgz", + "integrity": "sha512-b1wd1caegc1p18nTrfPhfHQAZW1GnWWKGldq5MZ8C/nkgJbjjN8SKb1Vw7GONkOnH6KxWDAXS4i93/wdQcz4Bg==", "dev": true, "dependencies": { "ajv": "8.12.0", "ajv-formats": "2.1.1", "jsonc-parser": "3.2.0", - "picomatch": "2.3.1", + "picomatch": "3.0.1", "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -256,79 +258,41 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.8.tgz", - "integrity": "sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.1.1.tgz", + "integrity": "sha512-3AtEO7k0Znzg11o51ZqebkW8063QkZ7Y7BC96Oye+wSdpT3ow57P0w0UtOpUNesNKzj1iMuPWqqm4i+YqitjCw==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.8", + "@angular-devkit/core": "17.1.1", "jsonc-parser": "3.2.0", - "magic-string": "0.30.0", + "magic-string": "0.30.5", "ora": "5.4.1", "rxjs": "7.8.1" }, "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", - "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" } }, "node_modules/@angular/animations": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz", - "integrity": "sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.1.1.tgz", + "integrity": "sha512-85qm8b4fNKa5zx4YlvCvAb3lWGlRsEcClnpCuwSVP8pXG7n/cW8MsJOh8i/wOkQTr9Ac7vvAE+yFWMi2A/qlTg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "16.2.12" + "@angular/core": "17.1.1" } }, "node_modules/@angular/cdk": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.12.tgz", - "integrity": "sha512-wT8/265zm2WKY0BDaRoYbrAT4kadrmejTRLjuimQIEUKnw4vBsJMWCwQkpFo3s6zr6eznGqYVAFb8KKPVLKGBg==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.1.0.tgz", + "integrity": "sha512-a2+uqr1s2pCStFs78BM1ViVqi0GnxFHGKHo58hiR9pDV/pyg9cvy+d+rsci1HkuF9AC/UqV5Y6rGLfwayO183g==", "dependencies": { "tslib": "^2.3.0" }, @@ -336,33 +300,33 @@ "parse5": "^7.1.2" }, "peerDependencies": { - "@angular/common": "^16.0.0 || ^17.0.0", - "@angular/core": "^16.0.0 || ^17.0.0", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/cli": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.8.tgz", - "integrity": "sha512-amOIHMq8EvixhnI+do5Bcy6IZSFAJx0njhhLM4ltDuNUczH8VH0hNegZKxhb8K87AMO8jITFM+NLrzccyghsDQ==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.1.1.tgz", + "integrity": "sha512-JG/Vf+PScR3PC6u7B+jFF4s5eBByzCpOfCfRFw98nlCqDAOxYOig7wi2Sbp5fnvILQH8vbc/NG8MzdgONrG6Hg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.8", - "@angular-devkit/core": "16.1.8", - "@angular-devkit/schematics": "16.1.8", - "@schematics/angular": "16.1.8", + "@angular-devkit/architect": "0.1701.1", + "@angular-devkit/core": "17.1.1", + "@angular-devkit/schematics": "17.1.1", + "@schematics/angular": "17.1.1", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", - "inquirer": "8.2.4", + "inquirer": "9.2.12", "jsonc-parser": "3.2.0", - "npm-package-arg": "10.1.0", - "npm-pick-manifest": "8.0.1", + "npm-package-arg": "11.0.1", + "npm-pick-manifest": "9.0.0", "open": "8.4.2", "ora": "5.4.1", - "pacote": "15.2.0", - "resolve": "1.22.2", - "semver": "7.5.3", + "pacote": "17.0.5", + "resolve": "1.22.8", + "semver": "7.5.4", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -370,112 +334,38 @@ "ng": "bin/ng.js" }, "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.1601.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.8.tgz", - "integrity": "sha512-kOXVGwsQnZvtz2UZNefcEy64Jiwq0eSoQUeozvDXOaYRJABLjPKI2YaarvKC9/Z1SGLuje0o/eRJO4T8aRk9rQ==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "16.1.8", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", - "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular/cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, - "node_modules/@angular/cli/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@angular/common": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.12.tgz", - "integrity": "sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.1.1.tgz", + "integrity": "sha512-YMM2vImWJg7H3Yaej7ncGpFKT28V2Y6X9/rLpRdSKAiUbcbj7GeWtX/upfZGR9KmD08baYZw0YTNMR03Ubv/mg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "16.2.12", + "@angular/core": "17.1.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.12.tgz", - "integrity": "sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.1.1.tgz", + "integrity": "sha512-lEQ5YNMJQm2iO2EZbGkwL3SqnxtE2ENfymgbS023F6ACsnP3kKB2DMwOnIbGgQY4+8r4sJFiMAIjEkj5c9kttg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "16.2.12" + "@angular/core": "17.1.1" }, "peerDependenciesMeta": { "@angular/core": { @@ -484,9 +374,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz", - "integrity": "sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.1.1.tgz", + "integrity": "sha512-d6Aev1P92q7wd5u3UcJifzNlU9svxaYI2Ts6MKoD4jY4/GPN/gPDqi20weDMujEgirrkcwGbsCXBRoEGkA5c9A==", "dev": true, "dependencies": { "@babel/core": "7.23.2", @@ -504,11 +394,11 @@ "ngcc": "bundles/ngcc/index.js" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "16.2.12", - "typescript": ">=4.9.3 <5.2" + "@angular/compiler": "17.1.1", + "typescript": ">=5.2 <5.4" } }, "node_modules/@angular/compiler-cli/node_modules/@babel/core": { @@ -556,145 +446,116 @@ "semver": "bin/semver.js" } }, - "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.3", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@angular/core": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.12.tgz", - "integrity": "sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.1.1.tgz", + "integrity": "sha512-JtNYM9eHr8eUSrGPq/kn0+/F+TSZ7EBWxZhM1ZndOlGu1gA4fGhrDid4ZXIHIs07DbM4NZjMn+LhRyx02YDsSA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.13.0" + "zone.js": "~0.14.0" } }, "node_modules/@angular/forms": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.12.tgz", - "integrity": "sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.1.1.tgz", + "integrity": "sha512-rqHVzaJDV8+VbnfC6mDgzX6ooa0X0hmnd+XfuOZaEJ7MtyOmqQ8qas2PAKXU7nMIImYXfYc4O4XWbSc1pRy1Hw==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "16.2.12", - "@angular/core": "16.2.12", - "@angular/platform-browser": "16.2.12", + "@angular/common": "17.1.1", + "@angular/core": "17.1.1", + "@angular/platform-browser": "17.1.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.12.tgz", - "integrity": "sha512-k1DGRfP1mMmhg/nLJjZBOPzX3SyAjgbRBY2KauKOV8OFCXJGoMn/oLgMBh+qB1WugzIna/31dBV8ruHD3Uvp2w==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/auto-init": "15.0.0-canary.bc9ae6c9c.0", - "@material/banner": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/button": "15.0.0-canary.bc9ae6c9c.0", - "@material/card": "15.0.0-canary.bc9ae6c9c.0", - "@material/checkbox": "15.0.0-canary.bc9ae6c9c.0", - "@material/chips": "15.0.0-canary.bc9ae6c9c.0", - "@material/circular-progress": "15.0.0-canary.bc9ae6c9c.0", - "@material/data-table": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dialog": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/drawer": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/fab": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", - "@material/form-field": "15.0.0-canary.bc9ae6c9c.0", - "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", - "@material/image-list": "15.0.0-canary.bc9ae6c9c.0", - "@material/layout-grid": "15.0.0-canary.bc9ae6c9c.0", - "@material/line-ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/linear-progress": "15.0.0-canary.bc9ae6c9c.0", - "@material/list": "15.0.0-canary.bc9ae6c9c.0", - "@material/menu": "15.0.0-canary.bc9ae6c9c.0", - "@material/menu-surface": "15.0.0-canary.bc9ae6c9c.0", - "@material/notched-outline": "15.0.0-canary.bc9ae6c9c.0", - "@material/radio": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/segmented-button": "15.0.0-canary.bc9ae6c9c.0", - "@material/select": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/slider": "15.0.0-canary.bc9ae6c9c.0", - "@material/snackbar": "15.0.0-canary.bc9ae6c9c.0", - "@material/switch": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab-bar": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab-indicator": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab-scroller": "15.0.0-canary.bc9ae6c9c.0", - "@material/textfield": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tooltip": "15.0.0-canary.bc9ae6c9c.0", - "@material/top-app-bar": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.1.0.tgz", + "integrity": "sha512-PzHTXAHuap4K7fteQhpR5+BCLV3jSpT9mhaN9evDGYqXS6iMcEX/9RBR7jVqtW2/5pnDopUioBc7pf0JWWI4JA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/auto-init": "15.0.0-canary.7f224ddd4.0", + "@material/banner": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/card": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/chips": "15.0.0-canary.7f224ddd4.0", + "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", + "@material/data-table": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dialog": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/drawer": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/fab": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/form-field": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/image-list": "15.0.0-canary.7f224ddd4.0", + "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/radio": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/slider": "15.0.0-canary.7f224ddd4.0", + "@material/snackbar": "15.0.0-canary.7f224ddd4.0", + "@material/switch": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/textfield": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tooltip": "15.0.0-canary.7f224ddd4.0", + "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/animations": "^16.0.0 || ^17.0.0", - "@angular/cdk": "16.2.12", - "@angular/common": "^16.0.0 || ^17.0.0", - "@angular/core": "^16.0.0 || ^17.0.0", - "@angular/forms": "^16.0.0 || ^17.0.0", - "@angular/platform-browser": "^16.0.0 || ^17.0.0", + "@angular/animations": "^17.0.0 || ^18.0.0", + "@angular/cdk": "17.1.0", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", + "@angular/platform-browser": "^17.0.0 || ^18.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.12.tgz", - "integrity": "sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.1.1.tgz", + "integrity": "sha512-/80znuEkdDvsF6EX/fe03isQlLCUS9+ldCgB4n0ZL+qAkf2/lJlU3n97SyEN7rzb189U+K1fDe0fb1nDwbbcWQ==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "16.2.12", - "@angular/common": "16.2.12", - "@angular/core": "16.2.12" + "@angular/animations": "17.1.1", + "@angular/common": "17.1.1", + "@angular/core": "17.1.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -703,36 +564,36 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz", - "integrity": "sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.1.1.tgz", + "integrity": "sha512-UKI8966nwo+p01EjmQdkepLIeVLpPZTSDZAM4va7CfMO6lbCN5xFecDd/sVbut8J6ySIsbJxyDkP+SHMQjE+xg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "16.2.12", - "@angular/compiler": "16.2.12", - "@angular/core": "16.2.12", - "@angular/platform-browser": "16.2.12" + "@angular/common": "17.1.1", + "@angular/compiler": "17.1.1", + "@angular/core": "17.1.1", + "@angular/platform-browser": "17.1.1" } }, "node_modules/@angular/router": { - "version": "16.2.12", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.12.tgz", - "integrity": "sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.1.1.tgz", + "integrity": "sha512-pPIRX0v8agij2dRSU25iwj9qFy0S25cztsy7bGfZ+M510jwRCqu1JsitqXtQ85XSv/bdFqiNiFU0UbwVFl+QiQ==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "16.2.12", - "@angular/core": "16.2.12", - "@angular/platform-browser": "16.2.12", + "@angular/common": "17.1.1", + "@angular/core": "17.1.1", + "@angular/platform-browser": "17.1.1", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -754,12 +615,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -767,34 +628,34 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", - "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.9", - "@babel/helper-module-transforms": "^7.22.9", - "@babel/helpers": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.8", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", + "json5": "^2.2.3", "semver": "^6.3.1" }, "engines": { @@ -805,6 +666,12 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -815,12 +682,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -854,14 +721,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -879,17 +746,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -937,9 +804,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -974,20 +841,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-function-name/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-hoist-variables": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", @@ -1135,9 +988,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1153,9 +1006,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1175,52 +1028,24 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -1232,9 +1057,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1275,35 +1100,30 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.3.tgz", - "integrity": "sha512-u8SwzOcP0DYSsa++nHd/9exlHb0NAlHCb890qtZZbSwPX2bFv8LBEztxwN7Xg/dS8oAFFidhrI9PBcLBJSkGRQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.7.tgz", + "integrity": "sha512-b1s5JyeMvqj7d9m9KhJNHKc18gEJiSyVzVX3bwbiPalQBQpuvfPh6lA9F7Kk/dWH0TIiXRpB9yicwijY6buPng==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-create-class-features-plugin": "^7.23.7", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", "@babel/plugin-syntax-decorators": "^7.23.3" }, "engines": { @@ -1325,23 +1145,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -1638,9 +1441,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.3.tgz", - "integrity": "sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", + "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -1656,14 +1459,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1688,9 +1491,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.3.tgz", - "integrity": "sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1719,9 +1522,9 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.3.tgz", - "integrity": "sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", @@ -1736,16 +1539,15 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", - "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", @@ -1774,20 +1576,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", @@ -1835,9 +1623,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.3.tgz", - "integrity": "sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1867,9 +1655,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.3.tgz", - "integrity": "sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1883,12 +1671,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1915,9 +1704,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.3.tgz", - "integrity": "sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1946,9 +1735,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.3.tgz", - "integrity": "sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2075,9 +1864,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.3.tgz", - "integrity": "sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2091,9 +1880,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.3.tgz", - "integrity": "sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2107,9 +1896,9 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.3.tgz", - "integrity": "sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", "dev": true, "dependencies": { "@babel/compat-data": "^7.23.3", @@ -2142,9 +1931,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.3.tgz", - "integrity": "sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2158,9 +1947,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.3.tgz", - "integrity": "sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2206,9 +1995,9 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.3.tgz", - "integrity": "sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -2270,16 +2059,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.9.tgz", - "integrity": "sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", + "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.4", - "babel-plugin-polyfill-corejs3": "^0.8.2", - "babel-plugin-polyfill-regenerator": "^0.5.1", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", "semver": "^6.3.1" }, "engines": { @@ -2375,13 +2164,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.3.tgz", - "integrity": "sha512-ogV0yWnq38CFwH20l2Afz0dfKuZBx9o/Y2Rmh5vuSS0YD1hswgEgTfyTzuSrT2q9btmHRSqYoSfwFUVaC1M1Jw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz", + "integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-create-class-features-plugin": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.23.3" }, @@ -2456,25 +2245,26 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz", - "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz", + "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -2486,59 +2276,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.7", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.5", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.5", - "@babel/plugin-transform-classes": "^7.22.6", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.5", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.5", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.5", - "@babel/plugin-transform-for-of": "^7.22.5", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.5", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-modules-systemjs": "^7.22.5", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.7", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", - "@babel/plugin-transform-numeric-separator": "^7.22.5", - "@babel/plugin-transform-object-rest-spread": "^7.22.5", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.6", - "@babel/plugin-transform-parameters": "^7.22.5", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.5", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.5", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.5", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.4", - "babel-plugin-polyfill-corejs3": "^0.8.2", - "babel-plugin-polyfill-regenerator": "^0.5.1", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -2559,14 +2348,12 @@ } }, "node_modules/@babel/preset-modules": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", - "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, @@ -2600,74 +2387,59 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", - "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "dev": true, "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", - "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.3", - "@babel/types": "^7.23.3", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.3", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -2693,10 +2465,26 @@ "node": ">=10.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", + "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz", - "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", + "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", "cpu": [ "arm" ], @@ -2710,9 +2498,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz", - "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", + "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", "cpu": [ "arm64" ], @@ -2726,9 +2514,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz", - "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", + "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", "cpu": [ "x64" ], @@ -2742,9 +2530,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz", - "integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", + "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", "cpu": [ "arm64" ], @@ -2758,9 +2546,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz", - "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", + "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", "cpu": [ "x64" ], @@ -2774,9 +2562,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz", - "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", + "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", "cpu": [ "arm64" ], @@ -2790,9 +2578,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz", - "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", + "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", "cpu": [ "x64" ], @@ -2806,9 +2594,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz", - "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", + "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", "cpu": [ "arm" ], @@ -2822,9 +2610,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz", - "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", + "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", "cpu": [ "arm64" ], @@ -2838,9 +2626,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz", - "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", + "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", "cpu": [ "ia32" ], @@ -2854,9 +2642,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz", - "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", + "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", "cpu": [ "loong64" ], @@ -2870,9 +2658,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz", - "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", + "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", "cpu": [ "mips64el" ], @@ -2886,9 +2674,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz", - "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", + "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", "cpu": [ "ppc64" ], @@ -2902,9 +2690,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz", - "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", + "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", "cpu": [ "riscv64" ], @@ -2918,9 +2706,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz", - "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", + "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", "cpu": [ "s390x" ], @@ -2934,9 +2722,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz", - "integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", + "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", "cpu": [ "x64" ], @@ -2950,9 +2738,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz", - "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", + "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", "cpu": [ "x64" ], @@ -2966,9 +2754,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz", - "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", + "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", "cpu": [ "x64" ], @@ -2982,9 +2770,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz", - "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", + "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", "cpu": [ "x64" ], @@ -2998,9 +2786,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz", - "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", + "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", "cpu": [ "arm64" ], @@ -3014,9 +2802,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz", - "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", + "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", "cpu": [ "ia32" ], @@ -3030,9 +2818,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz", - "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", + "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", "cpu": [ "x64" ], @@ -3045,11 +2833,14 @@ "node": ">=12" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "dev": true, + "engines": { + "node": ">=14" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -3233,9 +3024,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3249,766 +3040,766 @@ "dev": true }, "node_modules/@ljharb/through": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz", - "integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", + "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.5" }, "engines": { "node": ">= 0.4" } }, "node_modules/@material/animation": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-leRf+BcZTfC/iSigLXnYgcHAGvFVQveoJT5+2PIRdyPI/bIG7hhciRgacHRsCKC0sGya81dDblLgdkjSUemYLw==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-1GSJaPKef+7HRuV+HusVZHps64cmZuOItDbt40tjJVaikcaZvwmHlcTxRIqzcRoCdt5ZKHh3NoO7GB9Khg4Jnw==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/auto-init": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-uxzDq7q3c0Bu1pAsMugc1Ik9ftQYQqZY+5e2ybNplT8gTImJhNt4M2mMiMHbMANk2l3UgICmUyRSomgPBWCPIA==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-t7ZGpRJ3ec0QDUO0nJu/SMgLW7qcuG2KqIsEYD1Ej8qhI2xpdR2ydSDQOkVEitXmKoGol1oq4nYSBjTlB65GqA==", "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/banner": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-SHeVoidCUFVhXANN6MNWxK9SZoTSgpIP8GZB7kAl52BywLxtV+FirTtLXkg/8RUkxZRyRWl7HvQ0ZFZa7QQAyA==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/button": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-g9wBUZzYBizyBcBQXTIafnRUUPi7efU9gPJfzeGgkynXiccP/vh5XMmH+PBxl5v+4MlP/d4cZ2NUYoAN7UTqSA==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/base": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-Fc3vGuOf+duGo22HTRP6dHdc+MUe0VqQfWOuKrn/wXKD62m0QQR2TqJd3rRhCumH557T5QUyheW943M3E+IGfg==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-I9KQOKXpLfJkP8MqZyr8wZIzdPHrwPjFvGd9zSK91/vPyE4hzHRJc/0njsh9g8Lm9PRYLbifXX+719uTbHxx+A==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/button": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-3AQgwrPZCTWHDJvwgKq7Cj+BurQ4wTjDdGL+FEnIGUAjJDskwi1yzx5tW2Wf/NxIi7IoPFyOY3UB41jwMiOrnw==", - "dependencies": { - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BHB7iyHgRVH+JF16+iscR+Qaic+p7LU1FOLgP8KucRlpF9tTwIxQA6mJwGRi5gUtcG+vyCmzVS+hIQ6DqT/7BA==", + "dependencies": { + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/card": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-nPlhiWvbLmooTnBmV5gmzB0eLWSgLKsSRBYAbIBmO76Okgz1y+fQNLag+lpm/TDaHVsn5fmQJH8e0zIg0rYsQA==", - "dependencies": { - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kt7y9/IWOtJTr3Z/AoWJT3ZLN7CLlzXhx2udCLP9ootZU2bfGK0lzNwmo80bv/pJfrY9ihQKCtuGTtNxUy+vIw==", + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/checkbox": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-4tpNnO1L0IppoMF3oeQn8F17t2n0WHB0D7mdJK9rhrujen/fLbekkIC82APB3fdGtLGg3qeNqDqPsJm1YnmrwA==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-rURcrL5O1u6hzWR+dNgiQ/n89vk6tdmdP3mZgnxJx61q4I/k1yijKqNJSLrkXH7Rto3bM5NRKMOlgvMvVd7UMQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/chips": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-fqHKvE5bSWK0bXVkf57MWxZtytGqYBZvvHIOs4JI9HPHEhaJy4CpSw562BEtbm3yFxxALoQknvPW2KYzvADnmA==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/checkbox": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AYAivV3GSk/T/nRIpH27sOHFPaSMrE3L0WYbnb5Wa93FgY8a0fbsFYtSH2QmtwnzXveg+B1zGTt7/xIIcynKdQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "safevalues": "^0.3.4", "tslib": "^2.1.0" } }, "node_modules/@material/circular-progress": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-Lxe8BGAxQwCQqrLhrYrIP0Uok10h7aYS3RBXP41ph+5GmwJd5zdyE2t93qm2dyThvU6qKuXw9726Dtq/N+wvZQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/progress-indicator": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DJrqCKb+LuGtjNvKl8XigvyK02y36GRkfhMUYTcJEi3PrOE00bwXtyj7ilhzEVshQiXg6AHGWXtf5UqwNrx3Ow==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/data-table": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-j/7qplT9+sUpfe4pyWhPbl01qJA+OoNAG3VMJruBBR461ZBKyTi7ssKH9yksFGZ8eCEPkOsk/+kDxsiZvRWkeQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/checkbox": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", - "@material/linear-progress": "15.0.0-canary.bc9ae6c9c.0", - "@material/list": "15.0.0-canary.bc9ae6c9c.0", - "@material/menu": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/select": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-/2WZsuBIq9z9RWYF5Jo6b7P6u0fwit+29/mN7rmAZ6akqUR54nXyNfoSNiyydMkzPlZZsep5KrSHododDhBZbA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/density": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-Zt3u07fXrBWLW06Tl5fgvjicxNQMkFdawLyNTzZ5TvbXfVkErILLePwwGaw8LNcvzqJP6ABLA8jiR+sKNoJQCg==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-o9EXmGKVpiQ6mHhyV3oDDzc78Ow3E7v8dlaOhgaDSXgmqaE8v5sIlLNa/LKSyUga83/fpGk3QViSGXotpQx0jA==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/dialog": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-o+9a/fmwJ9+gY3Z/uhj/PMVJDq7it1NTWKJn2GwAKdB+fDkT4hb9qEdcxMPyvJJ5ups+XiKZo03+tZrD+38c1w==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/button": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-u0XpTlv1JqWC/bQ3DavJ1JguofTelLT2wloj59l3/1b60jv42JQ6Am7jU3I8/SIUB1MKaW7dYocXjDWtWJakLA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/dom": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-ly78R7aoCJtundSUu0UROU+5pQD5Piae0Y1MkN6bs0724azeazX1KeXFeaf06JOXnlr5/41ol+fSUPowjoqnOg==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-mQ1HT186GPQSkRg5S18i70typ5ZytfjL09R0gJ2Qg5/G+MLCGi7TAjZZSH65tuD/QGOjel4rDdWOTmYbPYV6HA==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/drawer": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-PFL4cEFnt7VTxDsuspFVNhsFDYyumjU0VWfj3PWB7XudsEfQ3lo85D3HCEtTTbRsCainGN8bgYNDNafLBqiigw==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/list": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-qyO0W0KBftfH8dlLR0gVAgv7ZHNvU8ae11Ao6zJif/YxcvK4+gph1z8AO4H410YmC2kZiwpSKyxM1iQCCzbb4g==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/elevation": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-Ro+Pk8jFuap+T0B0shA3xI1hs2b89dNQ2EIPCNjNMp87emHKAzJfhKb7EZGIwv3+gFLlVaLyIVkb94I89KLsyg==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-tV6s4/pUBECedaI36Yj18KmRCk1vfue/JP/5yYRlFNnLMRVISePbZaKkn/BHXVf+26I3W879+XqIGlDVdmOoMA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/fab": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-dvU0KWMRglwJEQwmQtFAmJcAjzg9VFF6Aqj78bJYu/DAIGFJ1VTTTSgoXM/XCm1YyQEZ7kZRvxBO37CH54rSDg==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-4h76QrzfZTcPdd+awDPZ4Q0YdSqsXQnS540TPtyXUJ/5G99V6VwGpjMPIxAsW0y+pmI9UkLL/srrMaJec+7r4Q==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/feature-targeting": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-wkDjVcoVEYYaJvun28IXdln/foLgPD7n9ZC9TY76GErGCwTq+HWpU6wBAAk+ePmpRFDayw4vI4wBlaWGxLtysQ==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SAjtxYh6YlKZriU83diDEQ7jNSP2MnxKsER0TvFeyG1vX/DWsUyYDOIJTOEa9K1N+fgJEBkNK8hY55QhQaspew==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/floating-label": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-bUWPtXzZITOD/2mkvLkEPO1ngDWmb74y0Kgbz6llHLOQBtycyJIpuoQJ1q2Ez0NM/tFLwPphhAgRqmL3YQ/Kzw==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-0KMo5ijjYaEHPiZ2pCVIcbaTS2LycvH9zEhEMKwPPGssBCX7iz5ffYQFk7e5yrQand1r3jnQQgYfHAwtykArnQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/focus-ring": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-cZHThVose3GvAlJzpJoBI1iqL6d1/Jj9hXrR+r8Mwtb1hBIUEG3hxfsRd4vGREuzROPlf0OgNf/V+YHoSwgR5w==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Jmg1nltq4J6S6A10EGMZnvufrvU3YTi+8R8ZD9lkSbun0Fm2TVdICQt/Auyi6An9zP66oQN6c31eqO6KfIPsDg==", "dependencies": { - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0" + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0" } }, "node_modules/@material/form-field": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-+JFXy5X44Gue1CbZZAQ6YejnI203lebYwL0i6k0ylDpWHEOdD5xkF2PyHR28r9/65Ebcbwbff6q7kI1SGoT7MA==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-fEPWgDQEPJ6WF7hNnIStxucHR9LE4DoDSMqCsGWS2Yu+NLZYLuCEecgR0UqQsl1EQdNRaFh8VH93KuxGd2hiPg==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/icon-button": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-1a0MHgyIwOs4RzxrVljsqSizGYFlM1zY2AZaLDsgT4G3kzsplTx8HZQ022GpUCjAygW+WLvg4z1qAhQHvsbqlw==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DcK7IL4ICY/DW+48YQZZs9g0U1kRaW0Wb0BxhvppDMYziHo/CTpFdle4gjyuTyRxPOdHQz5a97ru48Z9O4muTw==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/image-list": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-WKWmiYap2iu4QdqmeUSliLlN4O2Ueqa0OuVAYHn/TCzmQ2xmnhZ1pvDLbs6TplpOmlki7vFfe+aSt5SU9gwfOQ==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-voMjG2p80XbjL1B2lmF65zO5gEgJOVKClLdqh4wbYzYfwY/SR9c8eLvlYG7DLdFaFBl/7gGxD8TvvZ329HUFPw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/layout-grid": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-5GqmT6oTZhUGWIb+CLD0ZNyDyTiJsr/rm9oRIi3+vCujACwxFkON9tzBlZohdtFS16nuzUusthN6Jt9UrJcN6Q==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-veDABLxMn2RmvfnUO2RUmC1OFfWr4cU+MrxKPoDD2hl3l3eDYv5fxws6r5T1JoSyXoaN+oEZpheS0+M9Ure8Pg==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/line-ripple": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-8S30WXEuUdgDdBulzUDlPXD6qMzwCX9SxYb5mGDYLwl199cpSGdXHtGgEcCjokvnpLhdZhcT1Dsxeo1g2Evh5Q==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-f60hVJhIU6I3/17Tqqzch1emUKEcfVVgHVqADbU14JD+oEIz429ZX9ksZ3VChoU3+eejFl+jVdZMLE/LrAuwpg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/linear-progress": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-6EJpjrz6aoH2/gXLg9iMe0yF2C42hpQyZoHpmcgTLKeci85ktDvJIjwup8tnk8ULQyFiGiIrhXw2v2RSsiFjvQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/progress-indicator": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-pRDEwPQielDiC9Sc5XhCXrGxP8wWOnAO8sQlMebfBYHYqy5hhiIzibezS8CSaW4MFQFyXmCmpmqWlbqGYRmiyg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/list": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-TQ1ppqiCMQj/P7bGD4edbIIv4goczZUoiUAaPq/feb1dflvrFMzYqJ7tQRRCyBL8nRhJoI2x99tk8Q2RXvlGUQ==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Is0NV91sJlXF5pOebYAtWLF4wU2MJDbYqztML/zQNENkQxDOvEXu3nWNb3YScMIYJJXvARO0Liur5K4yPagS1Q==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/menu": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-IlAh61xzrzxXs38QZlt74UYt8J431zGznSzDtB1Fqs6YFNd11QPKoiRXn1J2Qu/lUxbFV7i8NBKMCKtia0n6/Q==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/list": "15.0.0-canary.bc9ae6c9c.0", - "@material/menu-surface": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-D11QU1dXqLbh5X1zKlEhS3QWh0b5BPNXlafc5MXfkdJHhOiieb7LC9hMJhbrHtj24FadJ7evaFW/T2ugJbJNnQ==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/menu-surface": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-dMtSPN+olTWE+08M5qe4ea1IZOhVryYqzK0Gyb2u1G75rSArUxCOB5rr6OC/ST3Mq3RS6zGuYo7srZt4534K9Q==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-7RZHvw0gbwppaAJ/Oh5SWmfAKJ62aw1IMB3+3MRwsb5PLoV666wInYa+zJfE4i7qBeOn904xqT2Nko5hY0ssrg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/notched-outline": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-WuurMg44xexkvLTBTnsO0A+qnzFjpcPdvgWBGstBepYozsvSF9zJGdb1x7Zv1MmqbpYh/Ohnuxtb/Y3jOh6irg==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Yg2usuKB2DKlKIBISbie9BFsOVuffF71xjbxPbybvqemxqUBd+bD5/t6H1fLE+F8/NCu5JMigho4ewUU+0RCiw==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/progress-indicator": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-uOnsvqw5F2fkeTnTl4MrYzjI7KCLmmLyZaM0cgLNuLsWVlddQE+SGMl28tENx7DUK3HebWq0FxCP8f25LuDD+w==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UPbDjE5CqT+SqTs0mNFG6uFEw7wBlgYmh+noSkQ6ty/EURm8lF125dmi4dv4kW0+octonMXqkGtAoZwLIHKf/w==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/radio": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-ehzOK+U1IxQN+OQjgD2lsnf1t7t7RAwQzeO6Czkiuid29ookYbQynWuLWk7NW8H8ohl7lnmfqTP1xSNkkL/F0g==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-wR1X0Sr0KmQLu6+YOFKAI84G3L6psqd7Kys5kfb8WKBM36zxO5HQXC5nJm/Y0rdn22ixzsIz2GBo0MNU4V4k1A==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/ripple": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-JfLW+g3GMVDv4cruQ19+HUxpKVdWCldFlIPw1UYezz2h3WTNDy05S3uP2zUdXzZ01C3dkBFviv4nqZ0GCT16MA==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-JqOsWM1f4aGdotP0rh1vZlPZTg6lZgh39FIYHFMfOwfhR+LAikUJ+37ciqZuewgzXB6iiRO6a8aUH6HR5SJYPg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/rtl": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-SkKLNLFp5QtG7/JEFg9R92qq4MzTcZ5As6sWbH7rRg6ahTHoJEuqE+pOb9Vrtbj84k5gtX+vCYPvCILtSlr2uw==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UVf14qAtmPiaaZjuJtmN36HETyoKWmsZM/qn1L5ciR2URb8O035dFWnz4ZWFMmAYBno/L7JiZaCkPurv2ZNrGA==", "dependencies": { - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/segmented-button": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-YDwkCWP9l5mIZJ7pZJZ2hMDxfBlIGVJ+deNzr8O+Z7/xC5LGXbl4R5aPtUVHygvXAXxpf5096ZD+dSXzYzvWlw==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LCnVRUSAhELTKI/9hSvyvIvQIpPpqF29BV+O9yM4WoNNmNWqTulvuiv7grHZl6Z+kJuxSg4BGbsPxxb9dXozPg==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/select": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-unfOWVf7T0sixVG+3k3RTuATfzqvCF6QAzA6J9rlCh/Tq4HuIBNDdV4z19IVu4zwmgWYxY0iSvqWUvdJJYwakQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", - "@material/line-ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/list": "15.0.0-canary.bc9ae6c9c.0", - "@material/menu": "15.0.0-canary.bc9ae6c9c.0", - "@material/menu-surface": "15.0.0-canary.bc9ae6c9c.0", - "@material/notched-outline": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-WioZtQEXRpglum0cMSzSqocnhsGRr+ZIhvKb3FlaNrTaK8H3Y4QA7rVjv3emRtrLOOjaT6/RiIaUMTo9AGzWQQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/shape": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-Dsvr771ZKC46ODzoixLdGwlLEQLfxfLrtnRojXABoZf5G3o9KtJU+J+5Ld5aa960OAsCzzANuaub4iR88b1guA==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8z8l1W3+cymObunJoRhwFPKZ+FyECfJ4MJykNiaZq7XJFZkV6xNmqAVrrbQj93FtLsECn9g4PjjIomguVn/OEw==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/slider": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-3AEu+7PwW4DSNLndue47dh2u7ga4hDJRYmuu7wnJCIWJBnLCkp6C92kNc4Rj5iQY2ftJio5aj1gqryluh5tlYg==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-QU/WSaSWlLKQRqOhJrPgm29wqvvzRusMqwAcrCh1JTrCl+xwJ43q5WLDfjYhubeKtrEEgGu9tekkAiYfMG7EBw==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/snackbar": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-TwwQSYxfGK6mc03/rdDamycND6o+1p61WNd7ElZv1F1CLxB4ihRjbCoH7Qo+oVDaP8CTpjeclka+24RLhQq0mA==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/button": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-sm7EbVKddaXpT/aXAYBdPoN0k8yeg9+dprgBUkrdqGzWJAeCkxb4fv2B3He88YiCtvkTz2KLY4CThPQBSEsMFQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/switch": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-OjUjtT0kRz1ASAsOS+dNzwMwvsjmqy5edK57692qmrP6bL4GblFfBDoiNJ6t0AN4OaKcmL5Hy/xNrTdOZW7Qqw==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-lEDJfRvkVyyeHWIBfoxYjJVl+WlEAE2kZ/+6OqB1FW0OV8ftTODZGhHRSzjVBA1/p4FPuhAtKtoK9jTpa4AZjA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", "safevalues": "^0.3.4", "tslib": "^2.1.0" } }, "node_modules/@material/tab": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-s/L9otAwn/pZwVQZBRQJmPqYeNbjoEbzbjMpDQf/VBG/6dJ+aP03ilIBEkqo8NVnCoChqcdtVCoDNRtbU+yp6w==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab-indicator": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-E1xGACImyCLurhnizyOTCgOiVezce4HlBFAI6YhJo/AyVwjN2Dtas4ZLQMvvWWqpyhITNkeYdOchwCC1mrz3AQ==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/tab-bar": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-Xmtq0wJGfu5k+zQeFeNsr4bUKv7L+feCmUp/gsapJ655LQKMXOUQZtSv9ZqWOfrCMy55hoF1CzGFV+oN3tyWWQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab-indicator": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab-scroller": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-p1Asb2NzrcECvAQU3b2SYrpyJGyJLQWR+nXTYzDKE8WOpLIRCXap2audNqD7fvN/A20UJ1J8U01ptrvCkwJ4eA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/tab-indicator": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-despCJYi1GrDDq7F2hvLQkObHnSLZPPDxnOzU16zJ6FNYvIdszgfzn2HgAZ6pl5hLOexQ8cla6cAqjTDuaJBhQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-h9Td3MPqbs33spcPS7ecByRHraYgU4tNCZpZzZXw31RypjKvISDv/PS5wcA4RmWqNGih78T7xg4QIGsZg4Pk4w==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/tab-scroller": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-QWHG/EWxirj4V9u2IHz+OSY9XCWrnNrPnNgEufxAJVUKV/A8ma1DYeFSQqxhX709R8wKGdycJksg0Flkl7Gq7w==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/tab": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LFeYNjQpdXecwECd8UaqHYbhscDCwhGln5Yh+3ctvcEgvmDPNjhKn/DL3sWprWvG8NAhP6sHMrsGhQFVdCWtTg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/textfield": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-R3qRex9kCaZIAK8DuxPnVC42R0OaW7AB7fsFknDKeTeVQvRcbnV8E+iWSdqTiGdsi6QQHifX8idUrXw+O45zPw==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/density": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", - "@material/line-ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/notched-outline": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AExmFvgE5nNF0UA4l2cSzPghtxSUQeeoyRjFLHLy+oAaE4eKZFrSy0zEpqPeWPQpEMDZk+6Y+6T3cOFYBeSvsw==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/theme": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-CpUwXGE0dbhxQ45Hu9r9wbJtO/MAlv5ER4tBHA9tp/K+SU+lDgurBE2touFMg5INmdfVNtdumxb0nPPLaNQcUg==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-hs45hJoE9yVnoVOcsN1jklyOa51U4lzWsEnQEuJTPOk2+0HqCQ0yv/q0InpSnm2i69fNSyZC60+8HADZGF8ugQ==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/tokens": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-nbEuGj05txWz6ZMUanpM47SaAD7soyjKILR+XwDell9Zg3bGhsnexCNXPEz2fD+YgomS+jM5XmIcaJJHg/H93Q==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-r9TDoicmcT7FhUXC4eYMFnt9TZsz0G8T3wXvkKncLppYvZ517gPyD/1+yhuGfGOxAzxTrM66S/oEc1fFE2q4hw==", "dependencies": { - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0" + "@material/elevation": "15.0.0-canary.7f224ddd4.0" } }, "node_modules/@material/tooltip": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-UzuXp0b9NuWuYLYpPguxrjbJnCmT/Cco8CkjI/6JajxaeA3o2XEBbQfRMTq8PTafuBjCHTc0b0mQY7rtxUp1Gg==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/button": "15.0.0-canary.bc9ae6c9c.0", - "@material/dom": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8qNk3pmPLTnam3XYC1sZuplQXW9xLn4Z4MI3D+U17Q7pfNZfoOugGr+d2cLA9yWAEjVJYB0mj8Yu86+udo4N9w==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "safevalues": "^0.3.4", "tslib": "^2.1.0" } }, "node_modules/@material/top-app-bar": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-vJWjsvqtdSD5+yQ/9vgoBtBSCvPJ5uF/DVssv8Hdhgs1PYaAcODUi77kdi0+sy/TaWyOsTkQixqmwnFS16zesA==", - "dependencies": { - "@material/animation": "15.0.0-canary.bc9ae6c9c.0", - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", - "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/shape": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", - "@material/typography": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SARR5/ClYT4CLe9qAXakbr0i0cMY0V3V4pe3ElIJPfL2Z2c4wGR1mTR8m2LxU1MfGKK8aRoUdtfKaxWejp+eNA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/touch-target": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-AqYh9fjt+tv4ZE0C6MeYHblS2H+XwLbDl2mtyrK0DOEnCVQk5/l5ImKDfhrUdFWHvS4a5nBM4AA+sa7KaroLoA==", - "dependencies": { - "@material/base": "15.0.0-canary.bc9ae6c9c.0", - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BJo/wFKHPYLGsRaIpd7vsQwKr02LtO2e89Psv0on/p0OephlNIgeB9dD9W+bQmaeZsZ6liKSKRl6wJWDiK71PA==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, "node_modules/@material/typography": { - "version": "15.0.0-canary.bc9ae6c9c.0", - "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.bc9ae6c9c.0.tgz", - "integrity": "sha512-CKsG1zyv34AKPNyZC8olER2OdPII64iR2SzQjpqh1UUvmIFiMPk23LvQ1OnC5aCB14pOXzmVgvJt31r9eNdZ6Q==", + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kBaZeCGD50iq1DeRRH5OM5Jl7Gdk+/NOfKArkY4ksBZvJiStJ7ACAhpvb8MEGm4s3jvDInQFLsDq3hL+SA79sQ==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", - "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", "tslib": "^2.1.0" } }, @@ -4023,9 +3814,9 @@ } }, "node_modules/@maxim_mazurok/gapi.client.drive-v3": { - "version": "0.0.20231107", - "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.0.20231107.tgz", - "integrity": "sha512-pzMiPZTSBYu+Hr7oZHfAgEA66xYA4Pj1IaNWahjmkKNu/t0oD276W85e9EBR9la/bpRyXOO2vQ9C4y9FpfLtaQ==", + "version": "0.0.20240118", + "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.0.20240118.tgz", + "integrity": "sha512-hCBTOZ0Tf1iWEVB3i0iyp4WRt7oTQFB/NpNl/ApI6YIRlv0RiiaQrhU9GG3kbpkGciLaeIJsNVt9Po0+chAq/A==", "dev": true, "dependencies": { "@types/gapi.client": "*", @@ -4033,18 +3824,18 @@ } }, "node_modules/@ngtools/webpack": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.10.tgz", - "integrity": "sha512-XAVn59zP3ztuKDtw92Xc9+64RK4u4c9g8y5GgtjIWeOwgNXl8bYhAo3uTZzrSrOu96DFZGjsmghFab/7/C2pDg==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.1.1.tgz", + "integrity": "sha512-uPWEpRuAUmMBZhYMpkEHNbeI8QAgeVM5lnWM+02lK75u1+sgYy32ed+RcRvEI+8hRQcsCQ8HtR4QubgJb4TzCQ==", "dev": true, "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "@angular/compiler-cli": "^16.0.0", - "typescript": ">=4.9.3 <5.2", + "@angular/compiler-cli": "^17.0.0", + "typescript": ">=5.2 <5.4", "webpack": "^5.54.0" } }, @@ -4083,6 +3874,31 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz", + "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/@npmcli/fs": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", @@ -4096,46 +3912,55 @@ } }, "node_modules/@npmcli/git": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", - "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz", + "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==", "dev": true, "dependencies": { - "@npmcli/promise-spawn": "^6.0.0", - "lru-cache": "^7.4.4", - "npm-pick-manifest": "^8.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^3.0.0" + "which": "^4.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { - "node": ">=12" + "node": "14 || >=16.14" } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/installed-package-contents": { @@ -4154,97 +3979,107 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", "dev": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@npmcli/move-file/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/@npmcli/package-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.0.tgz", + "integrity": "sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==", "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" }, "engines": { - "node": ">=10" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", "dev": true, + "dependencies": { + "which": "^4.0.0" + }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@npmcli/promise-spawn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", - "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "dependencies": { - "which": "^3.0.0" - }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=16" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/run-script": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", - "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", + "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", "dev": true, "dependencies": { "@npmcli/node-gyp": "^3.0.0", - "@npmcli/promise-spawn": "^6.0.0", - "node-gyp": "^9.0.0", - "read-package-json-fast": "^3.0.0", - "which": "^3.0.0" + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "which": "^4.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" } }, "node_modules/@npmcli/run-script/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/@pkgjs/parseargs": { @@ -4258,12 +4093,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", - "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz", + "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==", "dev": true, "dependencies": { - "playwright": "1.39.0" + "playwright": "1.41.1" }, "bin": { "playwright": "cli.js" @@ -4272,58 +4107,210 @@ "node": ">=16" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@schematics/angular": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.8.tgz", - "integrity": "sha512-gTHy1A/E9BCr0sj3VCr6eBYkgVkO96QWiZcFumedGnvstvp5wiCoIoJPLLfYaxVt1vt08xmnmS3OZ3r0qCLdpA==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.1.1.tgz", + "integrity": "sha512-1Wqefy1W9Y63g48Fp7BscL95V4U1seDGgZawH6DcJnytJVW89hazao7YREzLYfdoediuw7lU+OHJksWYe4VQww==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.8", - "@angular-devkit/schematics": "16.1.8", + "@angular-devkit/core": "17.1.1", + "@angular-devkit/schematics": "17.1.1", "jsonc-parser": "3.2.0" }, "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", - "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", + "node_modules/@sigstore/bundle": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz", + "integrity": "sha512-v3/iS+1nufZdKQ5iAlQKcCsoh0jffQyABvYIxKsZQFWc4ubuGjwZklFHpDgV6O6T7vvV78SW5NHI91HFKEcxKg==", "dev": true, "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" + "@sigstore/protobuf-specs": "^0.2.1" }, "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@sigstore/bundle": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-1.1.0.tgz", - "integrity": "sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==", + "node_modules/@sigstore/core": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-0.2.0.tgz", + "integrity": "sha512-THobAPPZR9pDH2CAvDLpkrYedt7BlZnsyxDe+Isq4ZmGfPy5juOFZq487vCU2EgKD7aHSiTfE/i7sN7aEdzQnA==", "dev": true, - "dependencies": { - "@sigstore/protobuf-specs": "^0.2.0" - }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sigstore/protobuf-specs": { @@ -4336,123 +4323,45 @@ } }, "node_modules/@sigstore/sign": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-1.0.0.tgz", - "integrity": "sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.1.tgz", + "integrity": "sha512-U5sKQEj+faE1MsnLou1f4DQQHeFZay+V9s9768lw48J4pKykPj34rWyI1lsMOGJ3Mae47Ye6q3HAJvgXO21rkQ==", "dev": true, "dependencies": { - "@sigstore/bundle": "^1.1.0", - "@sigstore/protobuf-specs": "^0.2.0", - "make-fetch-happen": "^11.0.1" + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", + "@sigstore/protobuf-specs": "^0.2.1", + "make-fetch-happen": "^13.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/sign/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@sigstore/sign/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sigstore/sign/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sigstore/sign/node_modules/make-fetch-happen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", - "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", - "dev": true, - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@sigstore/sign/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sigstore/sign/node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "node_modules/@sigstore/tuf": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.0.tgz", + "integrity": "sha512-S98jo9cpJwO1mtQ+2zY7bOdcYyfVYCUaofCG6wWRzk3pxKHVAkSfshkfecto2+LKsx7Ovtqbgb2LS8zTRhxJ9Q==", "dev": true, "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "@sigstore/protobuf-specs": "^0.2.1", + "tuf-js": "^2.2.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@sigstore/sign/node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/@sigstore/tuf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-1.0.3.tgz", - "integrity": "sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==", + "node_modules/@sigstore/verify": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-0.1.0.tgz", + "integrity": "sha512-2UzMNYAa/uaz11NhvgRnIQf4gpLTJ59bhb8ESXaoSS5sxedfS+eLak8bsdMc+qpNQfITUTFoSKFx5h8umlRRiA==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.2.0", - "tuf-js": "^1.1.7" + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", + "@sigstore/protobuf-specs": "^0.2.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sinclair/typebox": { @@ -4468,34 +4377,34 @@ "dev": true }, "node_modules/@stryker-mutator/api": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-7.3.0.tgz", - "integrity": "sha512-0tiQF0E38ypgg2fb2a4wbr2wpu4ugY7HwwsgrI9NttY1EojOS0BtaKHo1DIrj5SVMRXq0kaMgl5h2ohSuysvRA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-8.0.0.tgz", + "integrity": "sha512-4R8yieczpgOrOBQYq1d1aRJLcgenltpMH9ugtIYPPVmENFUo+mWm9HmaahLe+TVHnJSb4OZ4lvI7dVIhoBaiSQ==", "dev": true, "dependencies": { - "mutation-testing-metrics": "2.0.3", - "mutation-testing-report-schema": "2.0.3", + "mutation-testing-metrics": "3.0.0", + "mutation-testing-report-schema": "3.0.0", "tslib": "~2.6.0", "typed-inject": "~4.0.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/@stryker-mutator/core": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-7.3.0.tgz", - "integrity": "sha512-O9m2jEnJXbKBlj27/ps9nGCpm0HtQC0YlNV/aenocmERnySnvqEM6bwxvQ4apK5bad8ZyGJyhDIyJrwoVGmfVQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-8.0.0.tgz", + "integrity": "sha512-dyPs60wtLS9vgghgL5a49k/7KOovOnavXzm5SIBBsBJuxoO+5rvGXRohZaqSoZdb0yWIvp+LFdu6qWcdlJPkoQ==", "dev": true, "dependencies": { - "@stryker-mutator/api": "7.3.0", - "@stryker-mutator/instrumenter": "7.3.0", - "@stryker-mutator/util": "7.3.0", + "@stryker-mutator/api": "8.0.0", + "@stryker-mutator/instrumenter": "8.0.0", + "@stryker-mutator/util": "8.0.0", "ajv": "~8.12.0", "chalk": "~5.3.0", "commander": "~11.1.0", "diff-match-patch": "1.0.5", - "emoji-regex": "~10.2.1", + "emoji-regex": "~10.3.0", "execa": "~8.0.0", "file-url": "~4.0.0", "get-port": "~7.0.0", @@ -4504,9 +4413,9 @@ "lodash.groupby": "~4.6.0", "log4js": "~6.9.0", "minimatch": "~9.0.1", - "mutation-testing-elements": "2.0.3", - "mutation-testing-metrics": "2.0.3", - "mutation-testing-report-schema": "2.0.3", + "mutation-testing-elements": "3.0.1", + "mutation-testing-metrics": "3.0.0", + "mutation-testing-report-schema": "3.0.0", "npm-run-path": "~5.1.0", "progress": "~2.0.0", "rxjs": "~7.8.0", @@ -4521,22 +4430,7 @@ "stryker": "bin/stryker.js" }, "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@stryker-mutator/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18.0.0" } }, "node_modules/@stryker-mutator/core/node_modules/chalk": { @@ -4551,135 +4445,10 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@stryker-mutator/core/node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/@stryker-mutator/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@stryker-mutator/core/node_modules/color-name": { - "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 - }, - "node_modules/@stryker-mutator/core/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@stryker-mutator/core/node_modules/figures": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@stryker-mutator/core/node_modules/inquirer": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", - "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", - "dev": true, - "dependencies": { - "@ljharb/through": "^2.3.11", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^5.0.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/@stryker-mutator/core/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@stryker-mutator/core/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@stryker-mutator/core/node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@stryker-mutator/core/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@stryker-mutator/instrumenter": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-7.3.0.tgz", - "integrity": "sha512-RdfQF08GclNdKldG3rH9YztapPhfTYsc90p8Tev+b6yZJSpk1j8mKZRMjxk/mylDtXFZZ2IVhI9txAt2YYT+OQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-8.0.0.tgz", + "integrity": "sha512-kS3BdpqDuJeRBDBw2eP68rhUrgtlT5EGERNOwCwUGpfTtUan7y2zX9ZGEblOtMI5yTtyBArFZMtwhPGi+waJ/Q==", "dev": true, "dependencies": { "@babel/core": "~7.23.0", @@ -4687,161 +4456,79 @@ "@babel/parser": "~7.23.0", "@babel/plugin-proposal-decorators": "~7.23.0", "@babel/preset-typescript": "~7.23.0", - "@stryker-mutator/api": "7.3.0", - "@stryker-mutator/util": "7.3.0", - "angular-html-parser": "~4.0.0", + "@stryker-mutator/api": "8.0.0", + "@stryker-mutator/util": "8.0.0", + "angular-html-parser": "~5.0.0", + "semver": "~7.5.4", "weapon-regex": "~1.1.0" }, "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.3", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@stryker-mutator/instrumenter/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@stryker-mutator/instrumenter/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "node": ">=18.0.0" } }, "node_modules/@stryker-mutator/karma-runner": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@stryker-mutator/karma-runner/-/karma-runner-7.3.0.tgz", - "integrity": "sha512-q4pEkqbjer3UZWYDb64ts4wlNyeZD+n/0/SHqSOHeurTcRFywfhnzxt5hObKB0NKRRjq8/em/RUDu6t4gwmqYA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/karma-runner/-/karma-runner-8.0.0.tgz", + "integrity": "sha512-mW7OH4JBFTkrYxsCp1dHot7/sOGff5I1fSWgqus55yKReHao25hkdsR+9tAbKK9eDVIE2pdwfyHqTzLw8cftGg==", "dev": true, "dependencies": { - "@stryker-mutator/api": "7.3.0", - "@stryker-mutator/util": "7.3.0", + "@stryker-mutator/api": "8.0.0", + "@stryker-mutator/util": "8.0.0", "decamelize": "~6.0.0", "semver": "~7.5.0", "tslib": "~2.6.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "@stryker-mutator/core": "~7.3.0" + "@stryker-mutator/core": "~8.0.0" } }, "node_modules/@stryker-mutator/typescript-checker": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@stryker-mutator/typescript-checker/-/typescript-checker-7.3.0.tgz", - "integrity": "sha512-WNYeDRJNeyA6r6eSQLO9HPd0t88QD77Nv422EGzInGT9DIgpRffH46JijFsdcLZtzAq1OACFZZ25bgdHTEjCrQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/typescript-checker/-/typescript-checker-8.0.0.tgz", + "integrity": "sha512-8E6yci2yxsEYHy54xOpWSdBrLs2KDt/xl3tvxJc4hAwwIHY/Zni+scJ8kflpEZAashOB8W5Hn8f4aqF1TQ4ZKw==", "dev": true, "dependencies": { - "@stryker-mutator/api": "7.3.0", - "@stryker-mutator/util": "7.3.0", + "@stryker-mutator/api": "8.0.0", + "@stryker-mutator/util": "8.0.0", "semver": "~7.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "@stryker-mutator/core": "~7.3.0", + "@stryker-mutator/core": "~8.0.0", "typescript": ">=3.6" } }, "node_modules/@stryker-mutator/util": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-7.3.0.tgz", - "integrity": "sha512-bdFvuw7F3LC05dOFqgGjuipLt8ng5uXyjjdKeqqeTowm1wAyeDt0GTQKBuiINSAtcZxN75wTXq4DsCZXb/LMjw==", - "dev": true - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, - "engines": { - "node": ">= 6" - } + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-8.0.0.tgz", + "integrity": "sha512-geb2JvbYXDs6vOjGV9514PK+L6NiYFQIYhkpKvzfZQWYijNYnLhxXKz/X264a9StTYbGb/ZBADOkzsVde/5zig==", + "dev": true }, "node_modules/@tufjs/canonical-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", - "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@tufjs/models": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", - "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz", + "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==", "dev": true, "dependencies": { - "@tufjs/canonical-json": "1.0.0", - "minimatch": "^9.0.0" + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.3" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@types/body-parser": { @@ -4873,9 +4560,9 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.3.tgz", - "integrity": "sha512-6mfQ6iNvhSKCZJoY6sIG3m0pKkdUcweVNOLuBBKvoWGzl2yRxOJcYOTRyLKt3nxXvBLJWa6QkW//tgbIwJehmA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -4889,18 +4576,18 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", - "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/eslint": { - "version": "8.44.7", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", - "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -4999,9 +4686,9 @@ } }, "node_modules/@types/jasmine": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.2.tgz", - "integrity": "sha512-GJzYZWAr7aZuVsQwo77ErgdnqiXiz1lwsXXKgsJEwMlAxWQqjpiTGh0JOpLGXSlIFvIAFbgZTHs0u+jBzh/GFg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.4.tgz", + "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", "dev": true }, "node_modules/@types/json-schema": { @@ -5017,27 +4704,27 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "20.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", + "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/node-forge": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.9.tgz", - "integrity": "sha512-meK88cx/sTalPSLSoCzkiUB4VPIFHmxtXm5FaaqRDqBX2i/Sy8bJ4odsan0b20RBjPh06dAQ+OTTdnyQyhJZyQ==", + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/qs": { - "version": "6.9.10", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", "dev": true }, "node_modules/@types/range-parser": { @@ -5098,24 +4785,24 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", - "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", - "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz", + "integrity": "sha512-DKHKVtpI+eA5fvObVgQ3QtTGU70CcCnedalzqmGSR050AzKZMdUzgC8KmlOneHWH8dF2hJ3wkC9+8FDVAaDRCw==", "dev": true, "engines": { "node": ">=14.6.0" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, "node_modules/@webassemblyjs/ast": { @@ -5264,99 +4951,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@wessberg/ts-evaluator": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/@wessberg/ts-evaluator/-/ts-evaluator-0.0.27.tgz", - "integrity": "sha512-7gOpVm3yYojUp/Yn7F4ZybJRxyqfMNf0LXK5KJiawbPfL0XTsJV+0mgrEDjOIR6Bi0OYk2Cyg4tjFu1r8MCZaA==", - "deprecated": "this package has been renamed to ts-evaluator. Please install ts-evaluator instead", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "jsdom": "^16.4.0", - "object-path": "^0.11.5", - "tslib": "^2.0.3" - }, - "engines": { - "node": ">=10.1.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/wessberg/ts-evaluator?sponsor=1" - }, - "peerDependencies": { - "typescript": ">=3.2.x || >= 4.x" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/color-name": { - "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 - }, - "node_modules/@wessberg/ts-evaluator/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, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5375,17 +4969,14 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/accepts": { "version": "1.3.8", @@ -5401,31 +4992,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -5443,15 +5012,6 @@ "acorn": "^8" } }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", @@ -5480,27 +5040,15 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", "dev": true, "dependencies": { - "humanize-ms": "^1.2.1" + "debug": "^4.3.4" }, "engines": { - "node": ">= 8.0.0" + "node": ">= 14" } }, "node_modules/aggregate-error": { @@ -5562,12 +5110,12 @@ } }, "node_modules/angular-html-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-4.0.1.tgz", - "integrity": "sha512-x9SLf2jNNh3nG+haVIwKX/GVW8PcvSRmkeT9WqTDYSAVuwT9IzwEyVm09FCZpOo/dtFRxE9vaNXqcAf/MIxphg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-5.0.2.tgz", + "integrity": "sha512-fov2PwgZDgDsvZXPRa0+lbJyakOZOlFb5eiACR2i6RSn9ad5A+84/SwVfj/dUCbUAHH1ta2uvaoAKEijG93Sfg==", "dev": true, "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">= 14" @@ -5643,23 +5191,16 @@ "node": ">= 8" } }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/argparse": { @@ -5672,21 +5213,15 @@ } }, "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", "dev": true, "funding": [ { @@ -5696,12 +5231,16 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -5750,13 +5289,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -5773,25 +5312,41 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5907,13 +5462,11 @@ "dev": true }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -5945,16 +5498,10 @@ "node": ">=8" } }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -5971,9 +5518,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -6032,17 +5579,17 @@ } }, "node_modules/cacache": { - "version": "17.1.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", - "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", + "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", - "lru-cache": "^7.7.1", + "lru-cache": "^10.0.1", "minipass": "^7.0.3", - "minipass-collect": "^1.0.2", + "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", @@ -6051,16 +5598,16 @@ "unique-filename": "^3.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { - "node": ">=12" + "node": "14 || >=16.14" } }, "node_modules/call-bind": { @@ -6096,9 +5643,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001562", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001562.tgz", - "integrity": "sha512-kfte3Hym//51EdX4239i+Rmp20EsLIYGdPkERegTgU19hQWCRhsRFGKHTliUlsry53tv17K7n077Kqa0WJU4ng==", + "version": "1.0.30001580", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz", + "integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==", "dev": true, "funding": [ { @@ -6202,9 +5749,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", - "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -6214,12 +5761,12 @@ } }, "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/cliui": { @@ -6236,6 +5783,56 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "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 + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -6274,33 +5871,12 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -6421,12 +5997,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6518,12 +6088,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.2.tgz", - "integrity": "sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", "dev": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -6751,57 +6321,19 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "dependencies": { - "cssom": "~0.3.6" + "bin": { + "cssesc": "bin/cssesc" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, "node_modules/custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", "dev": true }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -6840,12 +6372,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true - }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -7000,21 +6526,6 @@ "node": ">=8" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7089,12 +6600,6 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -7145,27 +6650,6 @@ } ] }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -7208,15 +6692,15 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.585", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.585.tgz", - "integrity": "sha512-B4yBlX0azdA3rVMxpYwLQfDpdwOgcnLCkpvSOd68iFmeedo+WYjaBJS3/W58LVD8CB2nf+o7C4K9xz1l09RkWg==", + "version": "1.4.645", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.645.tgz", + "integrity": "sha512-EeS1oQDCmnYsRDRy2zTeC336a/4LZ6WKqvSaM1jLocEk5ZuyszkQtCpsqvuvaIXGOUjwtvF6LTcS8WueibXvSw==", "dev": true }, "node_modules/emoji-regex": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz", - "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, "node_modules/emojis-list": { @@ -7290,27 +6774,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -7386,9 +6849,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz", - "integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", + "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", "dev": true, "hasInstallScript": true, "bin": { @@ -7398,34 +6861,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.17", - "@esbuild/android-arm64": "0.18.17", - "@esbuild/android-x64": "0.18.17", - "@esbuild/darwin-arm64": "0.18.17", - "@esbuild/darwin-x64": "0.18.17", - "@esbuild/freebsd-arm64": "0.18.17", - "@esbuild/freebsd-x64": "0.18.17", - "@esbuild/linux-arm": "0.18.17", - "@esbuild/linux-arm64": "0.18.17", - "@esbuild/linux-ia32": "0.18.17", - "@esbuild/linux-loong64": "0.18.17", - "@esbuild/linux-mips64el": "0.18.17", - "@esbuild/linux-ppc64": "0.18.17", - "@esbuild/linux-riscv64": "0.18.17", - "@esbuild/linux-s390x": "0.18.17", - "@esbuild/linux-x64": "0.18.17", - "@esbuild/netbsd-x64": "0.18.17", - "@esbuild/openbsd-x64": "0.18.17", - "@esbuild/sunos-x64": "0.18.17", - "@esbuild/win32-arm64": "0.18.17", - "@esbuild/win32-ia32": "0.18.17", - "@esbuild/win32-x64": "0.18.17" + "@esbuild/aix-ppc64": "0.19.11", + "@esbuild/android-arm": "0.19.11", + "@esbuild/android-arm64": "0.19.11", + "@esbuild/android-x64": "0.19.11", + "@esbuild/darwin-arm64": "0.19.11", + "@esbuild/darwin-x64": "0.19.11", + "@esbuild/freebsd-arm64": "0.19.11", + "@esbuild/freebsd-x64": "0.19.11", + "@esbuild/linux-arm": "0.19.11", + "@esbuild/linux-arm64": "0.19.11", + "@esbuild/linux-ia32": "0.19.11", + "@esbuild/linux-loong64": "0.19.11", + "@esbuild/linux-mips64el": "0.19.11", + "@esbuild/linux-ppc64": "0.19.11", + "@esbuild/linux-riscv64": "0.19.11", + "@esbuild/linux-s390x": "0.19.11", + "@esbuild/linux-x64": "0.19.11", + "@esbuild/netbsd-x64": "0.19.11", + "@esbuild/openbsd-x64": "0.19.11", + "@esbuild/sunos-x64": "0.19.11", + "@esbuild/win32-arm64": "0.19.11", + "@esbuild/win32-ia32": "0.19.11", + "@esbuild/win32-x64": "0.19.11" } }, "node_modules/esbuild-wasm": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.18.17.tgz", - "integrity": "sha512-9OHGcuRzy+I8ziF9FzjfKLWAPbvi0e/metACVg9k6bK+SI4FFxeV6PcZsz8RIVaMD4YNehw+qj6UMR3+qj/EuQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.11.tgz", + "integrity": "sha512-MIhnpc1TxERUHomteO/ZZHp+kUawGEc03D/8vMHGzffLvbFLeDe6mwxqEZwlqBNY7SLWbyp6bBQAcCen8+wpjQ==", "dev": true, "bin": { "esbuild": "bin/esbuild" @@ -7458,37 +6922,6 @@ "node": ">=0.8.0" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -7502,15 +6935,6 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -7536,7 +6960,7 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { + "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -7545,6 +6969,15 @@ "node": ">=4.0" } }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7563,12 +6996,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter-asyncresource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", - "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", - "dev": true - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -7655,12 +7082,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -7778,9 +7199,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7800,9 +7221,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -7821,15 +7242,28 @@ } }, "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", + "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", "dev": true, "dependencies": { - "escape-string-regexp": "^1.0.5" + "escape-string-regexp": "^5.0.0", + "is-unicode-supported": "^1.2.0" }, "engines": { - "node": ">=8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7941,6 +7375,15 @@ "node": ">=8" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -7948,9 +7391,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -7983,20 +7426,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8089,31 +7518,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "dev": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8266,18 +7670,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/guess-parser": { - "version": "0.4.22", - "resolved": "https://registry.npmjs.org/guess-parser/-/guess-parser-0.4.22.tgz", - "integrity": "sha512-KcUWZ5ACGaBM69SbqwVIuWGoSAgD+9iJnchR9j/IarVI1jHVeXv+bUXBIMeqVMSKt3zrn0Dgf9UpcOEpPBLbSg==", - "dev": true, - "dependencies": { - "@wessberg/ts-evaluator": "0.0.27" - }, - "peerDependencies": { - "typescript": ">=3.7.5" - } - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -8329,12 +7721,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true - }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -8365,24 +7751,24 @@ "dev": true }, "node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", "dev": true, "dependencies": { - "lru-cache": "^7.5.1" + "lru-cache": "^10.0.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { - "node": ">=12" + "node": "14 || >=16.14" } }, "node_modules/hpack.js": { @@ -8427,18 +7813,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/html-entities": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", @@ -8538,17 +7912,16 @@ } }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", "dev": true, "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/http-proxy-middleware": { @@ -8576,16 +7949,16 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", "dev": true, "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { @@ -8597,15 +7970,6 @@ "node": ">=16.17.0" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8660,9 +8024,9 @@ } }, "node_modules/ignore-walk": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", - "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", + "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", "dev": true, "dependencies": { "minimatch": "^9.0.0" @@ -8733,12 +8097,6 @@ "node": ">=8" } }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8765,99 +8123,41 @@ } }, "node_modules/inquirer": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", - "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", + "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", "dev": true, "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", + "@ljharb/through": "^2.3.11", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^5.0.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=14.18.0" } }, - "node_modules/inquirer/node_modules/color-name": { - "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 - }, - "node_modules/inquirer/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==", + "node_modules/inquirer/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/ip": { @@ -8998,12 +8298,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -9017,12 +8311,12 @@ } }, "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9453,58 +8747,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9518,10 +8760,13 @@ } }, "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -9739,6 +8984,21 @@ "source-map-support": "^0.5.5" } }, + "node_modules/karma/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/karma/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -9760,6 +9020,24 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/karma/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/karma/node_modules/color-name": { + "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 + }, "node_modules/karma/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -9813,6 +9091,23 @@ "node": ">=8.17.0" } }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/karma/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -9869,9 +9164,9 @@ } }, "node_modules/less": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", - "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", "dev": true, "dependencies": { "copy-anything": "^2.0.1", @@ -10034,278 +9329,90 @@ }, "node_modules/log-symbols": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "dependencies": { - "chalk": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "dev": true, - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", - "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dev": true, - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/make-fetch-happen/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "chalk": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=4" } }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" }, "engines": { - "node": ">=8" + "node": ">=8.0" } }, - "node_modules/make-fetch-happen/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/make-fetch-happen/node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "dependencies": { - "minipass": "^3.1.1" + "@jridgewell/sourcemap-codec": "^1.4.15" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/make-fetch-happen/node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "unique-slug": "^3.0.0" + "semver": "^7.5.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-fetch-happen/node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "node_modules/make-fetch-happen": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", + "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", "dev": true, "dependencies": { - "imurmurhash": "^0.1.4" + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -10370,6 +9477,18 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -10474,70 +9593,34 @@ } }, "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", + "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", "dev": true, "dependencies": { - "minipass": "^3.1.6", + "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" }, "optionalDependencies": { "encoding": "^0.1.13" } }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-fetch/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", @@ -10700,9 +9783,9 @@ } }, "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true, "engines": { "node": ">=10" @@ -10728,31 +9811,34 @@ } }, "node_modules/mutation-testing-elements": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-2.0.3.tgz", - "integrity": "sha512-V00F5dVriVZTPoDcflX2Lp+/cA1LrkX9RwPntrrAEmM8OLEUG+jSZIJeYImTGK/opW5yD+q9ugykVjHbw2KQTg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.0.1.tgz", + "integrity": "sha512-hsBKkabjD2sjyR2vhdEFPDxZfYLw71geIWjEh4rcZSSQAtyWRfjGf6UbdMjleuyw1ZZTgGt6CImtwRY7s3lrVg==", "dev": true }, "node_modules/mutation-testing-metrics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-2.0.3.tgz", - "integrity": "sha512-pvrrE8Qf5xuimkm+TYUwX3g6Op6K4jE2/tD4NX8UZdTzuT/NHwAJw/YUXI7UJSA9M9Jpz9+VCjB31YnAX6wm7Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.0.0.tgz", + "integrity": "sha512-WslGuCdpqT+6SpeIahMhLrJl5+YbutlOCFKxuULIkAkaHfsWBK8UCq6euE7PiDEx+R1pYZo//kqRbFIOFmdQug==", "dev": true, "dependencies": { - "mutation-testing-report-schema": "2.0.3" + "mutation-testing-report-schema": "3.0.0" } }, "node_modules/mutation-testing-report-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-2.0.3.tgz", - "integrity": "sha512-+x6ssyq4xVkUyHbbbbiU1pCla7QHO/VRaxfHsOb4JGCw+56EtCJ4w4wQuQ24J5DYTRCAZ5y2oBk7DwP8UXWbwg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.0.0.tgz", + "integrity": "sha512-70+ZPYoyedruSGiEcXQnFiTtIusBYlsL/2EMwfR+/HOqBGxBpmI798spqc86ZVYXPVCL5mt2rWjE1dTQwcjpmQ==", "dev": true }, "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/nanoid": { "version": "3.3.7", @@ -10773,13 +9859,12 @@ } }, "node_modules/needle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", - "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "optional": true, "dependencies": { - "debug": "^3.2.6", "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, @@ -10790,16 +9875,6 @@ "node": ">= 4.4.x" } }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "optional": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/needle/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -10829,30 +9904,30 @@ "dev": true }, "node_modules/ng-mocks": { - "version": "14.11.0", - "resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.11.0.tgz", - "integrity": "sha512-6h0TafPogU7iEbWKGQt5npfEtI7IjThsqqnDboMIZ4AJyUY7VHUmhFa39Zkd2e4oOLDLb/6sVnfDIaWCp3oFgQ==", + "version": "14.12.1", + "resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.12.1.tgz", + "integrity": "sha512-5OdTYYOva7IkCCi6kTtgnII1hSfw+qYOM1ScrKhyo7iaI/ViV8xI4MGa89Ts7XnH6XqISSez2Un3zFSomkFpmg==", "dev": true, "funding": { "url": "https://github.com/sponsors/satanTime" }, "peerDependencies": { - "@angular/common": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16", - "@angular/core": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16", - "@angular/forms": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16", - "@angular/platform-browser": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16" + "@angular/common": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17", + "@angular/core": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17", + "@angular/forms": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17", + "@angular/platform-browser": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17" } }, "node_modules/ngx-filesize": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/ngx-filesize/-/ngx-filesize-3.0.2.tgz", - "integrity": "sha512-0h1gzcQYeNcGzDVcQcdhKK8RCrXdIXn4foT/DcXRwoht0KL3FQemJFKZowZmXESL7cnLlETaeRSnsHuqfchoWg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ngx-filesize/-/ngx-filesize-3.0.3.tgz", + "integrity": "sha512-qqP2p4WbbF7R+NXC9NqRQdAfWfMAYJ2Ijf4ezRCq7j3tPY6ybSP9AZ3FY1U7/95n1hmOJ2U5oY+oFb7LhHQRBw==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": ">= 14.2.0 < 17.0.0", - "@angular/core": ">= 14.2.0 < 17.0.0", + "@angular/common": ">= 14.2.0 < 18.0.0", + "@angular/core": ">= 14.2.0 < 18.0.0", "filesize": ">= 6.0.0 < 10.0.0" } }, @@ -10888,34 +9963,33 @@ } }, "node_modules/node-gyp": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", - "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz", + "integrity": "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==", "dev": true, "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", + "glob": "^10.3.10", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", "semver": "^7.3.5", "tar": "^6.1.2", - "which": "^2.0.2" + "which": "^4.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^12.13 || ^14.13 || >=16" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/node-gyp-build": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", - "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", "dev": true, "optional": true, "bin": { @@ -10924,82 +9998,64 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16" } }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" }, "engines": { - "node": "*" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", "dev": true, "dependencies": { - "abbrev": "^1.0.0" + "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", "dev": true, "dependencies": { - "hosted-git-info": "^6.0.0", + "hosted-git-info": "^7.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/normalize-path": { @@ -11050,160 +10106,67 @@ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", - "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^6.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-packlist": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", - "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", - "dev": true, - "dependencies": { - "ignore-walk": "^6.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-pick-manifest": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", - "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", - "dev": true, - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^10.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-registry-fetch": { - "version": "14.0.5", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", - "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", - "dev": true, - "dependencies": { - "make-fetch-happen": "^11.0.0", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^10.0.0", - "proc-log": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-registry-fetch/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm-registry-fetch/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npm-registry-fetch/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", - "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "node_modules/npm-package-arg": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", + "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", "dev": true, "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm-registry-fetch/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", "dev": true, + "dependencies": { + "ignore-walk": "^6.0.4" + }, "engines": { - "node": ">=8" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "node_modules/npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", "dev": true, "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm-registry-fetch/node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "node_modules/npm-registry-fetch": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.1.0.tgz", + "integrity": "sha512-PQCELXKt8Azvxnt5Y85GseQDJJlglTFM9L9U9gkv2y4e9s0k3GVDdOx3YoB6gm2Do0hlkzC39iCGXby+Wve1Bw==", "dev": true, + "dependencies": { + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^3.0.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm-run-path": { @@ -11233,21 +10196,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "dev": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -11260,12 +10208,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -11284,15 +10226,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-path": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz", - "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==", - "dev": true, - "engines": { - "node": ">= 10.12.0" - } - }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -11442,6 +10375,18 @@ "node": ">=8" } }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ora/node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11553,27 +10498,27 @@ } }, "node_modules/pacote": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", - "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.5.tgz", + "integrity": "sha512-TAE0m20zSDMnchPja9vtQjri19X3pZIyRpm2TJVeI+yU42leJBBDTRYhOcWFsPhaMxf+3iwQkFiKz16G9AEeeA==", "dev": true, "dependencies": { - "@npmcli/git": "^4.0.0", + "@npmcli/git": "^5.0.0", "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^6.0.1", - "@npmcli/run-script": "^6.0.0", - "cacache": "^17.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", "fs-minipass": "^3.0.0", - "minipass": "^5.0.0", - "npm-package-arg": "^10.0.0", - "npm-packlist": "^7.0.0", - "npm-pick-manifest": "^8.0.0", - "npm-registry-fetch": "^14.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", "proc-log": "^3.0.0", "promise-retry": "^2.0.1", - "read-package-json": "^6.0.0", + "read-package-json": "^7.0.0", "read-package-json-fast": "^3.0.0", - "sigstore": "^1.3.0", + "sigstore": "^2.0.0", "ssri": "^10.0.0", "tar": "^6.1.11" }, @@ -11581,16 +10526,7 @@ "pacote": "lib/bin.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/pacote/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/pako": { @@ -11629,6 +10565,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -11735,13 +10677,10 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, "engines": { "node": "14 || >=16.14" } @@ -11768,12 +10707,12 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -11790,12 +10729,11 @@ } }, "node_modules/piscina": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.0.0.tgz", - "integrity": "sha512-641nAmJS4k4iqpNUqfggqUBUMmlw0ZoM5VZKdQkV2e970Inn3Tk9kroCc1wpsYLD07vCwpys5iY0d3xI/9WkTg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.2.1.tgz", + "integrity": "sha512-LShp0+lrO+WIzB9LXO+ZmO4zGHxtTJNZhEO56H9SSu+JPaUQb6oLcTCzWi5IL2DS8/vIkCE88ElahuSSw4TAkA==", "dev": true, "dependencies": { - "eventemitter-asyncresource": "^1.0.0", "hdr-histogram-js": "^2.0.1", "hdr-histogram-percentiles-obj": "^3.0.0" }, @@ -11889,12 +10827,12 @@ } }, "node_modules/playwright": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", - "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz", + "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==", "dev": true, "dependencies": { - "playwright-core": "1.39.0" + "playwright-core": "1.41.1" }, "bin": { "playwright": "cli.js" @@ -11907,9 +10845,9 @@ } }, "node_modules/playwright-core": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", - "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz", + "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -11933,9 +10871,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "dev": true, "funding": [ { @@ -11952,7 +10890,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -11961,14 +10899,14 @@ } }, "node_modules/postcss-loader": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", - "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", "dev": true, "dependencies": { - "cosmiconfig": "^8.2.0", - "jiti": "^1.18.2", - "semver": "^7.3.8" + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "engines": { "node": ">= 14.15.0" @@ -11995,9 +10933,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -12012,9 +10950,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -12042,9 +10980,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -12170,12 +11108,6 @@ "dev": true, "optional": true }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12209,12 +11141,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12275,18 +11201,18 @@ "dev": true }, "node_modules/read-package-json": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", - "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.0.tgz", + "integrity": "sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==", "dev": true, "dependencies": { "glob": "^10.2.2", "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^5.0.0", + "normalize-package-data": "^6.0.0", "npm-normalize-package-bin": "^3.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/read-package-json-fast": { @@ -12302,24 +11228,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -12346,10 +11254,22 @@ "node": ">=8.10.0" } }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "dev": true }, "node_modules/regenerate": { @@ -12371,9 +11291,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "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": { @@ -12386,9 +11306,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": { @@ -12454,12 +11374,12 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -12581,9 +11501,9 @@ } }, "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "dev": true }, "node_modules/rimraf": { @@ -12644,25 +11564,41 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", "fsevents": "~2.3.2" } }, "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", "dev": true, "engines": { "node": ">=0.12.0" @@ -12731,9 +11667,9 @@ "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" }, "node_modules/sass": { - "version": "1.64.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz", - "integrity": "sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==", + "version": "1.69.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", + "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -12748,9 +11684,9 @@ } }, "node_modules/sass-loader": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", - "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", "dev": true, "dependencies": { "neo-async": "^2.6.2" @@ -12791,18 +11727,6 @@ "dev": true, "optional": true }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -12941,9 +11865,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -13033,22 +11957,17 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", "dev": true, "dependencies": { "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -13093,151 +12012,56 @@ "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sigstore": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.9.0.tgz", - "integrity": "sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A==", - "dev": true, - "dependencies": { - "@sigstore/bundle": "^1.1.0", - "@sigstore/protobuf-specs": "^0.2.0", - "@sigstore/sign": "^1.0.0", - "@sigstore/tuf": "^1.0.3", - "make-fetch-happen": "^11.0.1" - }, - "bin": { - "sigstore": "bin/sigstore.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/sigstore/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sigstore/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/sigstore/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/sigstore/node_modules/make-fetch-happen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", - "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", - "dev": true, - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/sigstore/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sigstore/node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=14" }, - "optionalDependencies": { - "encoding": "^0.1.13" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sigstore/node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "node_modules/sigstore": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.0.tgz", + "integrity": "sha512-fcU9clHwEss2/M/11FFM8Jwc4PjBgbhXoNskoK5guoK0qGQBSeUbQZRJ+B2fDFIvhyf0gqCaPrel9mszbhAxug==", "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", + "@sigstore/protobuf-specs": "^0.2.1", + "@sigstore/sign": "^2.2.1", + "@sigstore/tuf": "^2.3.0", + "@sigstore/verify": "^0.1.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/slash": { @@ -13263,9 +12087,9 @@ } }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", "dev": true, "dependencies": { "accepts": "~1.3.4", @@ -13289,27 +12113,6 @@ "ws": "~8.11.0" } }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -13349,17 +12152,17 @@ } }, "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", "dev": true, "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/source-map": { @@ -13381,17 +12184,16 @@ } }, "node_modules/source-map-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", - "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "dev": true, "dependencies": { - "abab": "^2.0.6", "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -13443,9 +12245,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -13770,6 +12572,15 @@ "node": ">=4" } }, + "node_modules/stryker-cli/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/stryker-cli/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -13873,12 +12684,6 @@ "node": ">=0.10" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -13957,9 +12762,9 @@ "dev": true }, "node_modules/terser": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", - "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -13975,16 +12780,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -14179,42 +12984,6 @@ "node": ">=0.6" } }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -14230,110 +12999,17 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tuf-js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", - "integrity": "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz", + "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==", "dev": true, "dependencies": { - "@tufjs/models": "1.0.4", + "@tufjs/models": "2.0.0", "debug": "^4.3.4", - "make-fetch-happen": "^11.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/tuf-js/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/tuf-js/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tuf-js/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/tuf-js/node_modules/make-fetch-happen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", - "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", - "dev": true, - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/tuf-js/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tuf-js/node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "make-fetch-happen": "^13.0.0" }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/tuf-js/node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/tunnel": { @@ -14397,9 +13073,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -14438,6 +13114,18 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/undici": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.2.1.tgz", + "integrity": "sha512-7Wa9thEM6/LMnnKtxJHlc8SrTlDmxqJecgz1iy8KlsN0/iskQXOQCuPkrZLXbElPaSw5slFFyKIKXyJ3UtbApw==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=18.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -14565,16 +13253,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14631,29 +13309,29 @@ } }, "node_modules/vite": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz", - "integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", + "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", "dev": true, "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.26", - "rollup": "^3.25.2" + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", @@ -14694,28 +13372,6 @@ "node": ">=0.10.0" } }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", - "dev": true, - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "dependencies": { - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -14753,19 +13409,10 @@ "integrity": "sha512-b0RmqduiSUKyKFamrpU+UK78Jm65/6CgKq1zoWFaS9PM7vwNK4RWrjmX1jREs3pLmG7botsgMLVOltxDR7RGRw==", "dev": true }, - "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true, - "engines": { - "node": ">=10.4" - } - }, "node_modules/webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -14920,9 +13567,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -14941,12 +13588,13 @@ } }, "node_modules/webpack-merge": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", - "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", + "flat": "^5.0.2", "wildcard": "^2.0.0" }, "engines": { @@ -15008,6 +13656,12 @@ "ajv": "^6.9.1" } }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -15055,35 +13709,6 @@ "node": ">=0.8.0" } }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -15099,15 +13724,6 @@ "node": ">= 8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -15115,9 +13731,9 @@ "dev": true }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "dependencies": { "ansi-styles": "^4.0.0", @@ -15125,10 +13741,7 @@ "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/wrap-ansi-cjs": { @@ -15222,12 +13835,12 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "dev": true, "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -15242,18 +13855,6 @@ } } }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -15309,9 +13910,9 @@ } }, "node_modules/zone.js": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.3.tgz", - "integrity": "sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.3.tgz", + "integrity": "sha512-jYoNqF046Q+JfcZSItRSt+oXFcpXL88yq7XAZjb/NKTS7w2hHpKjRJ3VlFD1k75wMaRRXNUt5vrZVlygiMyHbA==", "dependencies": { "tslib": "^2.3.0" } diff --git a/package.json b/package.json index 100a8ab..f83f174 100644 --- a/package.json +++ b/package.json @@ -17,16 +17,16 @@ "private": true, "//": "WARNING: Before upgrading angular, check compatibility with ngMocks (https://github.com/help-me-mom/ng-mocks)", "dependencies": { - "@angular/animations": "^16.1.0", - "@angular/cdk": "^16.1.4", - "@angular/common": "^16.1.0", - "@angular/compiler": "^16.1.0", - "@angular/core": "^16.1.0", - "@angular/forms": "^16.2.12", - "@angular/material": "^16.2.12", - "@angular/platform-browser": "^16.1.0", - "@angular/platform-browser-dynamic": "^16.2.12", - "@angular/router": "^16.2.12", + "@angular/animations": "^17.1.1", + "@angular/cdk": "^17.1.0", + "@angular/common": "^17.1.1", + "@angular/compiler": "^17.1.1", + "@angular/core": "^17.1.1", + "@angular/forms": "^17.1.1", + "@angular/material": "^17.1.0", + "@angular/platform-browser": "^17.1.1", + "@angular/platform-browser-dynamic": "^17.1.1", + "@angular/router": "^17.1.1", "@auth0/angular-jwt": "^5.2.0", "dexie": "^3.2.4", "dexie-export-import": "^4.0.7", @@ -34,16 +34,16 @@ "ngx-filesize": "^3.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", - "zone.js": "^0.13.3" + "zone.js": "^0.14.3" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.2.10", - "@angular/cli": "~16.1.3", - "@angular/compiler-cli": "^16.2.12", + "@angular-devkit/build-angular": "^17.1.1", + "@angular/cli": "~17.1.1", + "@angular/compiler-cli": "^17.1.1", "@playwright/test": "^1.39.0", - "@stryker-mutator/core": "^7.3.0", - "@stryker-mutator/karma-runner": "^7.3.0", - "@stryker-mutator/typescript-checker": "^7.3.0", + "@stryker-mutator/core": "^8.0.0", + "@stryker-mutator/karma-runner": "^8.0.0", + "@stryker-mutator/typescript-checker": "^8.0.0", "@types/gapi.client.drive-v3": "^0.0.4", "@types/google.accounts": "^0.0.14", "@types/google.picker": "^0.0.42", @@ -59,6 +59,6 @@ "ng-mocks": "^14.11.0", "strong-mock": "^8.0.1", "stryker-cli": "^1.0.2", - "typescript": "~5.1.6" + "typescript": "~5.3.3" } } From 1de7ba5e3bbfa75a59fe36994dcacc556484302f Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 25 Jan 2024 12:00:35 +0100 Subject: [PATCH 42/66] [us40] Support for reading pdf file content when running rules --- karma.conf.js | 10 +- package-lock.json | 1241 ++++++++++++++++------------ package.json | 4 +- src/app/rules/dummy.pdf | Bin 0 -> 13264 bytes src/app/rules/rule.service.spec.ts | 59 +- src/app/rules/rule.service.ts | 20 +- tsconfig.worker.json | 2 +- 7 files changed, 793 insertions(+), 543 deletions(-) create mode 100644 src/app/rules/dummy.pdf diff --git a/karma.conf.js b/karma.conf.js index e4fe692..954ff7c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -35,6 +35,14 @@ module.exports = function (config) { }, reporters: ['mocha', 'kjhtml'], browsers: ['ChromeHeadless'], - restartOnFileChange: true + restartOnFileChange: true, + files: [ + { + pattern: 'src/app/rules/dummy.pdf', + included: false, + watched: false, + served: true + } + ] }); }; diff --git a/package-lock.json b/package-lock.json index aa0f64f..732067c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "storemydocs", - "version": "0.3.1-12-g5d0e863", + "version": "0.3.1-44-gd69fd7b", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "storemydocs", - "version": "0.3.1-12-g5d0e863", + "version": "0.3.1-44-gd69fd7b", "dependencies": { "@angular/animations": "^17.1.1", "@angular/cdk": "^17.1.0", @@ -23,6 +23,7 @@ "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", "ngx-filesize": "^3.0.2", + "pdfjs-dist": "^3.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.14.3" @@ -2465,262 +2466,6 @@ "node": ">=10.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", @@ -2737,102 +2482,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@fastify/busboy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", @@ -3029,26 +2678,116 @@ "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@ljharb/through": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", + "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", - "dev": true + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/@ljharb/through": { - "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", - "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", - "dev": true, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, "dependencies": { - "call-bind": "^1.0.5" + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": ">= 0.4" + "node": ">=6" } }, "node_modules/@material/animation": { @@ -4107,109 +3846,92 @@ "node": ">=16" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", - "cpu": [ - "arm" - ], + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", "dev": true, "optional": true, - "os": [ - "linux" - ] + "peer": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", - "cpu": [ - "arm64" - ], + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "optional": true, - "os": [ - "linux" - ] + "peer": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", - "cpu": [ - "arm64" - ], + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", "dev": true, "optional": true, - "os": [ - "linux" - ] + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", - "cpu": [ - "riscv64" - ], + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "optional": true, - "os": [ - "linux" - ] + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.9.6", @@ -4237,44 +3959,26 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/wasm-node": { "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", - "cpu": [ - "x64" - ], + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.9.6.tgz", + "integrity": "sha512-B3FpAkroTE6q+MRHzv8XLBgPbxdjJiy5UnduZNQ/4lxeF1JT2O/OAr0JPpXeRG/7zpKm/kdqU/4m6AULhmnSqw==", "dev": true, "optional": true, - "os": [ - "win32" - ] + "peer": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } }, "node_modules/@schematics/angular": { "version": "17.1.1", @@ -4733,6 +4437,14 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -5161,7 +4873,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -5203,6 +4915,25 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -5356,7 +5087,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -5560,6 +5291,20 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -5662,6 +5407,21 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5713,7 +5473,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -5871,6 +5631,15 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -5892,6 +5661,14 @@ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -5956,7 +5733,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "devOptional": true }, "node_modules/connect": { "version": "3.7.0", @@ -5997,6 +5774,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6347,7 +6130,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "devOptional": true, "dependencies": { "ms": "2.1.2" }, @@ -6372,6 +6155,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -6526,6 +6332,12 @@ "node": ">=8" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6535,14 +6347,34 @@ "node": ">= 0.8" } }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true, + "engines": { + "node": ">=8" } }, "node_modules/detect-node": { @@ -6725,7 +6557,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -6735,7 +6566,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6978,6 +6808,14 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7493,7 +7331,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "devOptional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -7518,6 +7356,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7721,6 +7585,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -8101,7 +7971,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -8111,7 +7981,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "devOptional": true }, "node_modules/ini": { "version": "4.1.1", @@ -8122,6 +7992,17 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/injection-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.4.0.tgz", + "integrity": "sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.0.0" + } + }, "node_modules/inquirer": { "version": "9.2.12", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", @@ -8193,6 +8074,23 @@ "node": ">=8" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -8233,7 +8131,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -8265,6 +8163,14 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9534,6 +9440,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.7.6", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", @@ -9743,7 +9661,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -9756,7 +9674,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -9768,7 +9686,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/mkdirp": { "version": "0.5.6", @@ -9795,7 +9713,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "devOptional": true }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -9840,6 +9758,12 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -9918,6 +9842,128 @@ "@angular/platform-browser": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17" } }, + "node_modules/ng-packagr": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-17.1.2.tgz", + "integrity": "sha512-H7WRiqbM91lOItixrKc9XP1ZLpsxwIk3l0JDnhSePvKQlMe1UsNrnYHzBek6iVyMolCuz86YR0Dovbpyi4aOzA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@rollup/plugin-json": "^6.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/wasm-node": "^4.5.0", + "ajv": "^8.12.0", + "ansi-colors": "^4.1.3", + "browserslist": "^4.22.1", + "cacache": "^18.0.0", + "chokidar": "^3.5.3", + "commander": "^11.1.0", + "convert-source-map": "^2.0.0", + "dependency-graph": "^1.0.0", + "esbuild-wasm": "^0.19.5", + "fast-glob": "^3.3.1", + "find-cache-dir": "^3.3.2", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.2.0", + "less": "^4.2.0", + "ora": "^5.1.0", + "piscina": "^4.2.0", + "postcss": "^8.4.31", + "rxjs": "^7.8.1", + "sass": "^1.69.5" + }, + "bin": { + "ng-packagr": "cli/main.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "optionalDependencies": { + "esbuild": "^0.19.0", + "rollup": "^4.5.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0 || ^17.1.0-next.0 || ^17.2.0-next.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.2 <5.4" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/ng-packagr/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/ng-packagr/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/ng-packagr/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ng-packagr/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ng-packagr/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/ngx-filesize": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ngx-filesize/-/ngx-filesize-3.0.3.tgz", @@ -9953,6 +9999,26 @@ "dev": true, "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -10196,6 +10262,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -10212,7 +10290,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -10257,7 +10335,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -10640,7 +10718,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -10700,6 +10778,27 @@ "node": ">=8" } }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -11232,7 +11331,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11510,7 +11609,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, + "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -11525,7 +11624,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11535,7 +11634,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, + "devOptional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11555,7 +11654,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11639,7 +11738,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -11659,7 +11758,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/safevalues": { "version": "0.3.4", @@ -11769,7 +11868,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, + "devOptional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -11784,7 +11883,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -11796,7 +11895,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/send": { "version": "0.18.0", @@ -11957,6 +12056,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", @@ -12064,6 +12169,37 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -12341,7 +12477,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, + "devOptional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -12350,7 +12486,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -12385,13 +12521,13 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "devOptional": true }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -12697,7 +12833,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "dev": true, + "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -12714,7 +12850,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -12726,7 +12862,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -12738,7 +12874,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -12747,7 +12883,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -12759,7 +12895,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/terser": { "version": "5.26.0", @@ -12984,6 +13120,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -13257,7 +13399,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "devOptional": true }, "node_modules/utils-merge": { "version": "1.0.1", @@ -13409,6 +13551,12 @@ "integrity": "sha512-b0RmqduiSUKyKFamrpU+UK78Jm65/6CgKq1zoWFaS9PM7vwNK4RWrjmX1jREs3pLmG7botsgMLVOltxDR7RGRw==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, "node_modules/webpack": { "version": "5.89.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", @@ -13709,6 +13857,16 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -13724,6 +13882,15 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -13832,7 +13999,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "devOptional": true }, "node_modules/ws": { "version": "8.11.0", diff --git a/package.json b/package.json index f83f174..2647af8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "storemydocs", - "version": "0.3.1-12-g5d0e863", + "version": "0.3.1-44-gd69fd7b", "scripts": { "ng": "ng", "start": "ng serve", @@ -16,6 +16,7 @@ }, "private": true, "//": "WARNING: Before upgrading angular, check compatibility with ngMocks (https://github.com/help-me-mom/ng-mocks)", + "//2": "WARNING: Before upgrading pdfjs-dist, check comment response for (https://github.com/mozilla/pdf.js/issues/17245#issuecomment-1907799063)", "dependencies": { "@angular/animations": "^17.1.1", "@angular/cdk": "^17.1.0", @@ -32,6 +33,7 @@ "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", "ngx-filesize": "^3.0.2", + "pdfjs-dist": "^3.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.14.3" diff --git a/src/app/rules/dummy.pdf b/src/app/rules/dummy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..774c2ea70c55104973794121eae56bcad918da97 GIT binary patch literal 13264 zcmaibWmsIxvUW%|5FkJZ7A&~y%m9Oj;I6>~WPrgfxD$eVfZ*=#?hsspJHa(bATYRn zGueBev(G*EKHr+BrK+pDs^6;aH9u<6Dv3$30@ygwX}fZ|TDt1G($Rqw927PN=I8~c_R69-cY5S*jJE@5Wr0JUS6u!J~3#h`{ZMo=LkbbALoD8vfgB}Fh|2>mhOnfS$3 zNV5}8Ox=$fj;C0=UKy*{myZZPRVS|0mqr-HxZAy;()@wxQ}MN`QWAZTXb3Z&Om9W2 zbnA^OWoQbAW|3W^fw#J;YzDato8*`rHQs+@W70D&SyT{wb`SN*3nI z5G%$wJlq932=n{60Eii*9H8dFih2ks?QY=>nAFL=5g^P@#b{YUEHt0S$D7WbX zx%TzvzIK%zpvzLEd9LNr0ch#LFf_(9 zEGt0C9v~%b54vynAc{~;v&2?S(-sTTft@9CABMNFZHtY1W0-99CEbUNfp_yu{LDBz z@8z^$LPN$wX4Hi+dZQs6K3QiKKF0}Nme@EII;;F}IplC(YvT*C3-Oh#(A}e5pIz01 zyR}D2|ftBF0T=1moHZy}$wS*PSCmSzHQ%x z2tCQQCx4jt7w1cuhY69~eH`31KC4)ZZJ^)f=IabocAkBPa zEeg25yPX&9-i_N(Qiq!I3RDrfx&0t^i)&MSQ1D(w%|%#LTNr>1cPiltAYO;6kBn(B?r11c^Bz~#)z5~~V+*`U)lDFtKbZ|;? z&4wTUtK=KE&uQIWUQv1mDE;LIhXXgx44PMa@%Z<7a& zx45^oYSnei^~%}`?!O-+cgfSmn_c?`=Gmm*Z^I(96ve&$zDs|)r84)IEEiE1kfQ$q zm3km*m1)PjdU9nkk9BTlidI1~M|O~WfP7AUu2T}d>5is9l$<%;7r2&Re06w>W$KM~ zqITBTd=Ln>^crw`_N?{ z;2d_=E0n!*NisQ|XYuX9q3+UcqdA(MC45|>2tz^c6HdZOmXTB?X2Elx@_0f)1z&-gS;UxN`>Ll-kWb0X0 zTrQis=w9sJ(q7k|@|k3SA~DJ@uMXP@4(Mgn+LJC+3F~3NHW71pIzY(aHg~{O+squi zWO_|F>78)L5*gcRXXRD9IzQ(ddSxh}E7(8sC~EYrOz$9BkSMBCkGGO9FuZ{#*mW+h zvwE7d)6Ag=a*R5URs>}qdqb_E6g)kN2Wel;pWe9=hZ)XvRZR!RQg&gxAPGj8J0!gR zrdV<2@MZQ?_Ocbd5@0zI?t>$z3eD80_h^{DI)H5lk`T4lbn8kteH3%fOBH^g26#lLN2&P^s zr&d05GDs)u_8OKzCgNxllk5pLC<2wKmghL{zW%}5^}%S$?d=3OzjaSzT3>uWYikZN z2ZcR7*L|%UMs|u)wMi7#vkN?cxlBcyAM80Tyzzv&zHMF1TH9?Mx5&E57P^)^zE5N| z^foq}!--if$Uj=U6Tc>EM!Pv)e^_SZSdvtQ=@>)(ONejQ!XW8u6>ESl<*s^6cH;Q1 z#n}nL{#|{l}}@td^zNSA;R{`3A&Jjr8L9(3^2FSyZ1W9$%;!XP#N2 z-SAzyRfxtgq^py7_3*GJFO%x_v<`xJ46`~S*IukgQDKfLxzFnS&GYL!1LA{I z!c#{A90{k(b*tUfbgjOH>}{#V;%^O+LUU<*#QkLtWzjho*Kb?Cr&wC38%wxpn}^Wy zG6EpV9x3xioCWA6H6=aE3)%jmZePu#Ji7wy0CmkDZNG`a{J1i-2`Bt&UrFb&<~V$^ zy9i`R1<35M&{mtCz144%v#7LKBTPPApjoV}#W-gDc5cn;A@Mbt#zXUK@J9^vj*ME( zo8(%K{c-KDr8n1-I&Mjn)*i|pF|7l*`fXvo8-z&j{$NOfUPM-xILbX1D29IHp|__B zL*JQ8*7-VrZVY*&$!PiE%zv@osg`qx0M8+w9iy7Az7;HYezs;5NRvrdNM~t@o}5Gc zjagk3Y_>6!Ct;ITqhu3FojJO^(^SG-($M4|frkp?4y-QoSmFcw9Z%(z?eC0kGi9@? zm(vAgXU|%!6_)CrnqYL-Hj@B5hA?#8C3G^cjd?0dMSZ!wbe%O4bWvlIG=nwOEInVj zhjzd`Bry8sXBTfIUr+juZH5JyE#7~UQiwR!gmG@wm}aNyo`13xEo)tzP64MWWG|j8 z8u8a2_=C2FdRZ9(eG&Au`@$mY9vvWldP-@wj5@38H0W2V8wnaQO?!)qoS_J=(ieoI zOvH}mkBRh_p1oTW66+?3u-GH2Ex~c=BQiwpJ zJlF7O2PBaCojRRL_mp44*Iq}vcRFpBD>V9M7do5{w&b;4^<_V~Vr{+O_&hz9k5Sm` zq3|%Z(6B5~wz2k0iH-QlafAa>1%ZebdxkR;6SdA?@dK|4Jf8PIO%64Fpw$6RYG2R# zX>Iq(xf`5Xk)79-@;BAQjlWu|w@Ss3sJv3Ew&%lBu-H?vYsC8XPJD!lkv*A~z_-k= zLOaM?B5}$Sf-KF5BWHoB51WFA{GlweQna618{*tqVn)YKUVq?khU_=QER9uW?N17xgAponbjg0W`=>f;sulH3?st)Y_@k$We2-__a>^{E78lUiI13qq!3# zwxMEl75MK1q`~J>ST#?`mUx#vr%-jwpZ+DV;W!0KNkZmO#sK)zt)H@`EQl6RRWhwb z0&E7|fG~@z)wlK1-RsxN#8Gr)D5=xpv=b}=CWPbwz@(9bIhD0Crd-Q>qEo>~Gh{X7 z77AK5>TfF0wK!?7Nx!<5uDy?D{Qg$SEc_R3J9EuH!Z@qmEJ*QRRHd3BPirM6783nv zAnab$>rhdDJ6pO@%Ox(}BYw{Ba<3|=A%Fg5_Hfxj{%CfzZCFO{?%h&=?%CNBvi&p; z(otqN>+5giLLa^*G?xzN30=IgQrV+r7dW4bX;zKtuD)O$UnwAKC?CpkPt{77nUArH ze-jKcCfRrOlp(Q^b&W}mrgt4n%wikNxeSBBE_n>K-IOIzi6!<)xGRYA)wGgqp^s@d46N#krDHPc#9SOgXhI7Vbj?B z%c6@8dCOGPYBoNE#3N7HD^ihbC9*xGm6chu;?fcuv)s01keHHZ1vXl5D;29O7wZBr zyPzyLZHKMtUI%PK+*X2zTFtaDzU1qn(H=hRRj-SoJw7I5i%4b0u=&InEAKgoae-lp zXk0SkjlJ52HruS*1QykTZ&aCN`PbcKuw$1st{peJ@&aF^aR@~{XA@L&YvK%+VU}G4 ze5iuesu&i6=*#nvHbm_v-ZLr5^Ij#|YSAper4XpsH;0x(2h1-tIobIy;0~2a( z!G($SB!iu#P;;hGeI~C`O=-3|d~zoB0!`*JrU-)Ko_X5#kSpy5o^z49RG;{j#l~45 zF?X9Ih4IdviT(8@+q|`BveLTprbESZ6^2I&ew|V3pDXRe9gSyXT)zzqKQ;gCD;p+( zM)2(;YJ%P5)X(N3ZSn>dn6UIcEcvQOXZBn}uD!7V0yXr$f+d@eTSYoquPit2S8cPW zA8t3dX)Cv{0cKF`@e|PP(xS0|z2_R0(P6)#+kC$0^5- z$7Hs|bOQanE z1oJ;uh(dYiDt}mVmtC3&HaGT6-dY429v#ySHJ7V)C8ow=PSmnEI)=b3_RJsU(S*+J zV$p3>RkK?DFvTc;(-T=h!1u~CP!pE=0eSSu#c@N7S0Z57CPg}!5z{QL#`2v?DJDt^ zCGN{0p-&&=)Sb28Xlo;ZXc^CGdwL9prf30uu$y5aPeWD6WIk4%%~DEhTiwOvy!rS% z&3z#DWo2qBA*=M2xIu=_R0sbrmP;Y?_rRa^k}3WYU6n9H^(})Zi-woMKKXfgbab@J zWx3DUr0MLpdDYk_LO8As}d*Z=x^K+uIv#T&SnY6&C$9 zBn1u`G#TBt+n5b%a;Cr0h^sm5Fl^OdxJ^8IebW);DWATq#Ba=#rggj*wNKy5NMzz& zBm`bk9bcSVPJbC`dHrI>o^=LSvTFpT`VAK`x_naOpvS~*l2$1vIk$avBA!|aeZ+7c z$_9Zzh>fc4$uX&w@-$VORCscG(B)OA@SPj>BNY3gxkkcPgNi9bE=?&3A4`3ekrdsb zn~`M;p8I>4?@@ZI{9Afv(tC@pp@Oe5BYUw-%&J_WaTBGls)&d8q?t$i<<@=_CNfH! z4H!ww7#gkp_^`bxZaJI9@C+A9x7@E1ZRoG5PL?w3GDi>`8Qq%I+0ygfT78%{Zt#mP zqX0CzaHKn@hAOQsv=^8UbfpuyFnT8Ht++Vmmx$~09!e{5t8fMkEjr~tfIxMlIpr4zGwvEIWKC2`Q#C)c7QF9wet?hE zLKoU?t@nqm=iBc` z8_((*(i(g}7z)3{%SJ!uya{?Ir-2^Fiap*VC4pF@N zpL5F*DG+(taLhdu4DbyAP(0&60n@%?G~hHugBI^-X6@_YOu}8UqwbQ8V`2vwDRLMz z)aRFo+r1f?5idT9xRF`cjgx$a-IpH3AH|bs$emw}d23*3aU0hYNh4(D0o-Z+wIX{d zeann?lzjgsAt62`er@<$`G755?i7tl%CHNgXp}#j>j&S1n5wZ;ofNbI>B2*4L1}@3 zq(LzPqn()w{KBsX!5*a&=dv<}t=R%II;TcQatbnKM7S4Q1PQIoT=^$#=>Y(m{mBYtl5W z6}|l4kxikOcJ`C3o{TSxIi?8|N6sH7Lkhq5qttl@uBTA|-cBluU$hU0&xYKvNidrL z4q>|j76}G1Db23Fa|XlFm%W&jW0h#7B$_FD-ZhqJ5#7i!0ZmCrereX z|Jlf`<1zR2akFe|boWv-r=}kM03o|%$mZA7Of2T99u~e56~6sh$P=yk9f!H6msn)n zvFOLF?W?iqi6fK9C)a42Sgt0kz4#M6 z-UY6451Er~=V;ITs1O-q*>}{;bs74MMZ(Z&=Z{5#q+i@cw^vI#0|Dh~-Dh-tn2I(S zTXXp-bLEG{p0#BbIqIcTM|DWZmr`&br8u)jQ`CR*^+g_fIX%=K+)x}F%Oak-Uh$6nIHUavnNV5M7YffU80QPRD%y>T{bIzn<6Rsy zb6cW6`?0EwSn;uJddPn@`?^Cry2s(6ccP1ykKr!kmDg2~zbTJq@+e(z5N>ZNr|8$j zPi-~ofp7E|Xx1#H+f@UR@AS}iLP!}}dRwf{u!avAq-_hNw#uaoOD{2jo*eRn8$~bDK`h1&ssOC6ekGV38+hU!KR z+kpnSzT;y#o|V2h|F?SY4-z1MFxz0;)@Lk`H>Cj zSl@fR%*@F79;HJcsX%L8_d!%TwmQyi$|n&C{oBMJ9~Xm!@@#lZdz(WB9SgJ#NIC%@ zy+~ZnI|4E`7f@W0Y9I@N7UTs1fTPD-ZiU%Lr2MnP+2h8AGh?(WGVf>h@W-_M>jRkD z(KNxvo(UJ7)o+*t%fCcM10;2XM$1NAFKwhp(c917^io_ynn-yv58IFIF*UJUw*2Ma zm?a-a1yp9B?WxpLzap-c^$HKkX_IfT_W8Lqaltl*A%vZSZWAe`Kv}vjz}>Tc;Hw9T zA+Nc49X&{WDmxY~ReV0YceXdL!$9mTL$Q@_vXIW6I{G=`$KR7jFcE&IsHwnKX;KldV#YL z(xwKAB5cFiz+r6m*5iJvo&E)XQqVWjmA}BfyVS&dm9&Y%$Sp^sW!JE3iI0v(kQHdo zmhWk|gC!e@CFKPv4BE*U;mYo0y}J0J-Fhu!c%v+paQf9+3Ed2EkfPt(D7|Ok#t)^PGr3Y)RGfvO=k;@Xry=Cf3fLCQ# zi`%oCt+vyB-t{iEgI&+2dczmnMXj>EOmSpMuuL8Ob`1$D;fc$wM6j2HH4Q$ zqaoj&M$2sLhpptdJMbs!krJId=iOd}HdP4Lt@yf42OZ{pOoQ4_gShz_sMoWYX}yQd zDQ8(tc7UvTt%`0#?9K!C^J>GpucEnBhnsWg102Z=uzOlwez^q^j7nV$krID#wC}A$ zcRfc2)T5Y~({6@1`{yL-Lzs;miT@C9|1SIFBMK7cz*E;v2H|EStZphjfb5mGMpw{q z!pl;Vw772tuvDH4o$;j4u8)@=m+&BIf4Ix(u75P?Q{4Y8^uvpq)mCW(enuQc)hx$B zOY{`_*%~bm%k*x6y;)D8_-yYbMsC8y#1H}89X;M=a#*HT>d*NFf}x$pQ&X?nFtvzA zKH|l8y;frsm|&}<%&*}Yu}Yn0M=Jy8qe%<1qXRR%Nut}Aqr+1pQS*D7Cp`+8Y`RO02p14DyVOmSYlEzZ;9&JzYhtybMZ%e4s zlks=V(+aJ!LK-()3ox`%9c)lx#3#y4{ulL6KpG|&>9`n?Uh#m3G-mZy-3h98Scyja zH^3Pb7?P z+2hAkyvg}g$#)n$Gs2fL19JNOZ|~>Nx(|}lmwesC!>?Y~72mpf4XZ8t^TIwbCk;i0 z+a2ymSZ^=OrtrSH!(y#Vn!8KWk#O7<1-!if+`dDDy18U7wS3k$lIeM}Z0fhYqI)+x zo*o4*S$S|hGf6vL>PaQ(OQ_%eskx-G-FV|dXHbTH<#w@RbeIx9I$d$xqHh`{*&d3y zevlYNk)}w@cuu4A$^DYJsOvO7VBaom@Rx@gb$V5IKJ{Xue16H-1H0j=U0brW-aVRG znWCQRkESBmD^4?a7mB@!jf2>(Hs=Bd-;XX1oEilevb9axB^NhIPLO>jl03S+Rw|fx z&oIsIk(~W!4$zzKF|uSR<@S#;{r;fKup)iDaxz_9JouroY>XHcrN(Mm@UHV?-8bCh zXGfY~7U`rCasv(h-R*ava)^ zF1`BMT*n3xQBTdM?`n&h2Ecf*XXuLo7Zyl_El(v~oh>}mK01$%0a@#uzyiX_g>Bav2XWwH%YekAxU%pBT!p*?%cS#zA zv;^eDC#KZP@7o=^GDc_V8<3w>`*L(+=A#(fcH)dGjqM}Vk_el+c>B`{9xm<>IZ-Zm zLL!-Yf*3nju_(8ZGUd9*K`iofWW+BYFnZF&+a|=yxqV?oUOcG#ulnSR$DMs|e5Tph%WW zVjzE3nMh7+rG!}av)+~;o$#+EHyPX zzOUO?^#)Jh*t^b7pTW+I%f;xy&JMPCO&5RR``BmHX-Mw{qoJp9BjKea$;A9%>-iEZ zvuUBm%0j5UWax~`ue!K6dDdip+zs3f{+qQKqH;9C(1Z@95()-Ew=`BdLh2VS3zI8qYGH&&7m9+vpUc+x8l!i-ATXKhw34XL2;ya_VIQz!OL^)8mtqnb?q=~&^h-$;Zn^HRZ2p(gH z39An;`AWT=i&VP0u&CUe7OYW51Icv=q%Vc7%Zm z_uAp9n}osEUdk2*pV)*i`WRSa-FWtCwGqS-75@K#V0)r;+0(0XVp9vnb7lWiMj!q= z>Zf(ioa@gSwA55Jil$lh)%4U<)$j@HTQU2KwuUUsZA*2O^QTKobak8g0Qb~ROMTW7 zfTF2yF*na6i(lQ*Nq^rPen^0>$$b`K!Kp{FVa-VF`kCiXZg0Vtr}i*rcpny_YOR!} z+?Jiv?dWlT`}o$s9Fxt%%684d7ek-q-Q~jS*I5+8HtvSw+Rp!D=+gVr!gqcYy9K74 z&eClx6f6{1Din;ynjz?XZlJ~W7^A@0wiHIt8$aou;f>MYpU%gUlDwAK*nX0#vHtyl z_C=B+ZkOffY|oR^2>(+IlZCTMFirZMhn>bqzR=38hvJpcM4-@gUYY7_k^G*FW9;5r zc9q4c>C?hd{uS3{MThN*(w!3e05e?bI#SNlo$U&%>((Dz0_JeqbG|}!wI$& z%q2JQ)Vas;i0RYqNXW!CC~QK%u$K$beGI zT2KuzMjus26(zmofK;m2gY%d*o~sHBKA#`RBNc9c*-GLmbgh?*9V;^TBSot2E%~Q5 zl+R!WA_h_JT;+irbJ#Z-tSy-;B^t&&dOSwPV(T!CB)no8Y4sP%k(MD^0P!NL1vK&7 z`3luW2$gkI#Zf>IZT2=m4R&e@d zeo#B=Q|9`w8}%|)f%GBjYO01&Dk5qjm$+#1yia#CE=Sh~88Vdp%|VU}0a6mF@JkhUY&~W3f#rHK-1Qdo z>0*z5?#-hQUY}k^X7~1bkI?($-~3#c3mF4Cl@2%|0@1=ARZ z^qlNaN63&>;O_~mmto}?tAhznb}p;GpyIq1Z^yf<_6Ui~cpbbP;uV7W!+ke>wYG-f zPPz2~%UgSs(>vsKFle%uo=WIDYz;BR!doAy)aQ0QCpE_Wz1XK+3Kpr=V_H8w zqzaizn9ALx#?fo-N)_CtENYH*1|ID|x=xa9d#;9~1Wgrcx^8=evrfky*Xj`269~A;kh^O|ewZnM}=SmM7NX=?h#jjLh&1kIT+A z)If4luYo@s+e_L&eRJ$gw1`)>u#efOq=M0iYIPS$GII0z`T56eNxK@~Y%*^~Q&w$1b)jM9Z~kuRc~YX`6r#ySCskW5cq|#a39s;ZiaL~OdEpgu z1k*sKkLZ&?6fAi=)77yKI1xii%)@DG8r}663xkJcwLTj?s`h{GP@_2}`A|;w7zrzk4QOQ*O$(e|M^<`vLD*1^i>Nr*= z+A`y@f{!zLi)ys9OrFM5`Qw0292Ciyq>zC>8(TkG1O;#UUh?#I08kuwpS_vhufJ0v&p^Yr`=^WG7!qVG(8n9u7=J64fr zQq7B|9rzl7s)I_|8UeVp?=cqGILQ}0O(n+^vJz=vFBU9JmG$=DWzi+qCHw@D0a7`M zA`%pmU8+8W{u0{2*^tg&3;I&i`4`{YJe_n8 z{viTJZL?$}#l9w${3mydrW>Z%nY!WXf$HJv5$Zw4F%7^mXWsZ-s&olv31;C*KlH)j z?j?Eika^cI`l>)WJ*ga?%>0HwJm{%<)OP8pdvwMG@fm;Ca`jfy7ixY-sic42*f&ld zJg3(O0~;=Zsp@cdUj@&Zj~#~LX=F5Ws@!Ik0-~(wlbJO6&)S~s6WrAW9lrQ%6+S03 z&P&xJ{;BC%2s%J#uxZy3=Fc}fkwE9(T}QAK9b{FT!L3^PQ~;#X$T|9v&JFq)ru$h|ls zvPxYyWT}V&Dol3#)t6pVE4nIClEq=r++eGcG-tkOW4{n$Ra~3z?`@_gXRUiR`SrhY4K z#>C+t>pNtm>!Zw*;p^qI0|g<)Ob`r0jaN6asw2ZGLT}bMbHnQ$OH8cR7{Rq?=4%&x z2Qe&O`w$~b%fuo>fkgT`PVx=uto@&SdDpIXL)<da|A*x(b?o zdUj^iN+B9%;2{1URo7=%m@r*RJi3fQNO_`AZY;b#tClm;A}NQF#!Y;pMMdh=^fO@9 z>J>Xv^joKJM>M7x=xh!oSLO3JlxVwTn$DPHdGsnkAvB)9d)IE6ZHgd1vd+Z;W1d682CBy4zti z&6;T6!rzSKIy&zKKfAx9J%7q-=Mac{u-_GIYEaZt*`h25Ne?ch`E_c2{pGA<;nVkx z102u6#||N$g5MhA{!rFwaI(;8$S{1DePGc^L~j6?Q$2QMIO09 zPdma#_kX(|;oOau(pX877ac9V4O8x3g{Mdbr6oS)7 zN0v#H_j!bhUNl;q>GrkeA~){;lCg@&Mg5(z%E1HV`d7{>_}@9JZ(VJn>=HKC4q{My zLpw8D2OD@&E}T?=SV7rE-XI?4H+E(aOI8sZOC$NW=!leE6MG6ycn2;fB4XpB!^#Z= zQ?P=-+!R0#4h{+c2LPbUF6{uZG&6i-ZDI+f;6P`8V{ZtxcA((p;6i6ds6r4x005m` z6k;m{H8U}FK+J;+syaZe)G2u2J;eI(G+`)^0+C~@0#BIzJLi_?-}e8NR15?I|34|k zx>2LneiYApj|7nW4k1sp9h-vz^G);Jq7ONB*clw!(IJ2QT3sYWS)>yb_Ual2Um3r5 zw706UJD48HLY73$&Gm=sl|EYND&Uk>VT!eN_p49f6HS<{TU>u{4&#WYh1dwy^E8il ziH`_=$2m8k)y$Q2yDZQluP+AZbND!Yi7Co@fwHnw2pV1bo*=wGx2n7Urt$y1@imz1&#&nK47Nw zT-dLY@^1NHY?5B#-Qf9?`lA_={@NnLpmwJGQG7&oU}0>) ziZ`GdjY(jIKi2Q?e+d=de}nq3pkP;ZG;lyf$Xh!{=x?qF#2$)p%>NM^W_I=tqNWf# zgv;e1fAtY=)-W@2FtyhKb8%3Bfj|mw00#vR4=)857d&XdU z(4fLD4>dA_AWjHkeJ)-u3LZ|NF1w_ijiW6*A6^xXD#Y5}7O{k(E4!#F{9rhl8A4Sg zMcAb&9N>rx39*a9v4(4~r$8jq|MLt0{*hTPYU2nu0sub&aQG~$!9>qU@%LGVw1{ZAdD5crj3WAdl2KV62-uIT7sX=aUZ*>8aV1F3(c z_P=p-FtxG!8!9*^U<3>RcoByeFaipAK|lhB5)AqaI)n^@hmeEwxOw0OKK@%C0pZ{C z5o^F{FbEE(DEt!$_$B<8DlYiaV7ME855ql#Py+_S#o(c8`L;d6lqRR~$cn(zq-4};(pf)4`xt=`PWS`7YO27?$MdgtpDP{`vCa4 z{2x3Z5bm@8-~oUj5Zv+q!Gl}N`CoDX0N4M*gTIpgb1nb?;)Y)s|FIqb0Ot6gw!m#h zTnhg~j+YZ2)c?r?0yzIm4hZ1=FTFrc;D6}=a`OJeW(PY6{AFi{I1;L6ZcsR+>?$@k z@FNVDLEL!K*2XpzfZwk|I3Y%%Lm?mm76XGtKw?0k2(JV$kO#;s#>p!o!6gRf5#f;l j@(7{-|3%=32kuUL2Z)`+Z(jm{U>-0!Ev>ks1p5C2Hj`#V literal 0 HcmV?d00001 diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 74c7e1e..e443901 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -235,7 +235,64 @@ describe('RuleService', () => { await runAllPromise; // No failure in mock setup })); - // TODO: only keep one file content in memory and only if the file type content can be fetched + + it('should automatically categorize a file (using pdf file content)', fakeAsync(async () => { + // Arrange + let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + .thenReturn(progress); + when(() => progress.next({ + index: 1, + value: 0, + description: "Downloading file content of 'dummy.pdf'" + })).thenReturn(); + when(() => progress.next({ + index: 2, + value: 0, + description: "Running rule 'Dummy' for 'dummy.pdf'" + })).thenReturn(); + + when(() => progress.next({ + index: 2, + value: 100, + })).thenReturn(); + + let fileService = mockFileService(); + when(() => fileService.findOrCreateFolder("Dummy", "baseFolderId")) + .thenReturn(mustBeConsumedAsyncObservable('dummyCatId548')); + + let file = mockFileElement('dummy.pdf'); + file.mimeType = 'application/pdf'; + let dummyPdfResponse = await fetch('/base/src/app/rules/dummy.pdf'); + let dummyPdfBlob = await dummyPdfResponse.blob(); + when(() => fileService.downloadFile(file, progress)) + .thenReturn(mustBeConsumedAsyncObservable(dummyPdfBlob)); + + // The file should be set to the bills category + when(() => fileService.setCategory(file.id, 'dummyCatId548')) + .thenReturn(mustBeConsumedAsyncObservable(undefined)); + + const service = MockRender(RuleService).point.componentInstance; + + let ruleRepository = ngMocks.findInstance(RuleRepository); + when(() => ruleRepository.findAll()) + .thenResolve([{ + name: 'Dummy', + category: ['Dummy'], + script: 'return fileContent.startsWith("Dummy");' + }]); + + mockFilesCacheService([file], true); + + // Act + let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); + + // Assert + // fakeAsync(() => tick()); + await runAllPromise; + // No failure in mock setup + })); }) }); diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 6059bd5..7ce0f9f 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -6,6 +6,8 @@ import {Rule, RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; import {BackgroundTaskService, Progress} from "../background-task/background-task.service"; import {fromPromise} from "rxjs/internal/observable/innerFrom"; +import * as pdfjs from "pdfjs-dist"; +import {TextItem} from "pdfjs-dist/types/src/display/api"; export interface RuleResult { rule: Rule, @@ -30,6 +32,8 @@ export class RuleService { private filesCacheService: FilesCacheService, private backgroundTaskService: BackgroundTaskService) { // Create a new this.worker = new Worker(new URL('./rule.worker', import.meta.url)); + const pdfWorkerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`; + pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerSrc; } runAll(): Observable { @@ -81,7 +85,19 @@ export class RuleService { description: "Downloading file content of '" + file.name + "'" }); fileContentObservable = this.fileService.downloadFile(file, progress) - .pipe(mergeMap(blobContent => fromPromise(blobContent.text()))); + .pipe(mergeMap(blobContent => { + if (file.mimeType === 'application/pdf') { + return fromPromise(blobContent.arrayBuffer() + .then(arrayBuffer => pdfjs.getDocument(arrayBuffer).promise) + .then(pdfDocument => pdfDocument.getPage(1)) + .then(firstPage => firstPage.getTextContent()) + .then(textContent => textContent.items + .filter((item): item is TextItem => item !== undefined) + .map(item => "" + item.str).join())); + } else { + return fromPromise(blobContent.text()); + } + })); } else { fileContentObservable = of(""); } @@ -113,7 +129,7 @@ export class RuleService { } private isFileContentReadable(file: FileElement) { - return file.mimeType.startsWith('text/'); + return file.mimeType.startsWith('text/') || file.mimeType === 'application/pdf'; } private run(rule: Rule, file: FileElement, fileContent: string, progress: BehaviorSubject, progressIndex: number): Observable { diff --git a/tsconfig.worker.json b/tsconfig.worker.json index 22dc454..1ac0261 100644 --- a/tsconfig.worker.json +++ b/tsconfig.worker.json @@ -4,7 +4,7 @@ "compilerOptions": { "outDir": "./out-tsc/worker", "lib": [ - "es2018", + "es2022", "webworker" ], "types": [] From 51b87d55df185ec8e80295d57fe28a610f558460 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 25 Jan 2024 15:27:58 +0100 Subject: [PATCH 43/66] [us40] Support for rule modification --- src/app/rules/rule.repository.spec.ts | 29 ++++++++++++ src/app/rules/rule.repository.ts | 7 +++ src/app/rules/rule.service.spec.ts | 3 +- src/app/rules/rule.service.ts | 5 +++ src/app/rules/rules.component.html | 30 ++++++++----- src/app/rules/rules.component.scss | 2 +- src/app/rules/rules.component.spec.ts | 64 ++++++++++++++++++++++++--- src/app/rules/rules.component.ts | 52 ++++++++++++++-------- 8 files changed, 154 insertions(+), 38 deletions(-) diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index e04bb83..2cb772e 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -120,6 +120,35 @@ describe('RuleRepository', () => { expect(rules).toEqual([]); }); }); + + describe('update', () => { + it('should update an existing rule', async () => { + // Arrange + const ruleRepository = MockRender(RuleRepository).point.componentInstance; + // 2 calls to 'backup' expected, from create, and then from update + mockBackupCall().times(2); + let rule: Rule = { + name: 'TestRule', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }; + await ruleRepository.create(rule) + rule.name = 'TestRule edited'; + + // Act + await ruleRepository.update(rule) + + // Assert + let rules = await db.rules.toArray(); + expect(rules) + .toEqual([{ + id: 1, + name: 'TestRule edited', + category: ['Test1', 'ChildTest1'], + script: 'return true' + }]); + }) + }) }); export function mockRuleRepository() { diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index fca6279..3bc4da2 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -30,4 +30,11 @@ export class RuleRepository { this.databaseBackupAndRestoreService.backup().subscribe(); } } + + async update(rule: Rule) { + if (rule.id) { + await db.rules.update(rule.id, rule); + this.databaseBackupAndRestoreService.backup().subscribe(); + } + } } diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index e443901..e5963d1 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -307,7 +307,8 @@ export function mockRuleService() { runAll: ruleServiceMock.runAll, create: ruleServiceMock.create, findAll: ruleServiceMock.findAll, - delete: ruleServiceMock.delete + delete: ruleServiceMock.delete, + update: ruleServiceMock.update } }); return ruleServiceMock; diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 7ce0f9f..9be90db 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -68,6 +68,10 @@ export class RuleService { return this.ruleRepository.delete(rule); } + update(rule: Rule): Promise { + return this.ruleRepository.update(rule); + } + /** * Run the given rules on the given files and return the associated category for each file that got a matching rule */ @@ -167,6 +171,7 @@ export class RuleService { } // TODO: move and refactor duplicate to FileService + private findOrCreateCategories(categories: string[], categoryId: string): Observable { let categoryName = categories.shift(); if (categoryName !== undefined) { diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 7784536..2f89319 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -5,22 +5,22 @@

Setup rules

- +  
-
+
Name - +   Category - - {{cat}} + + {{ cat }} @@ -34,28 +34,34 @@

Setup rules

Script - +
- +   - + +
- {{rule.name}} + {{ rule.name }} -  > {{cat}} +  > {{ cat }}
- {{rule.script}} + {{ rule.script }} +
+
+ +   +
-
diff --git a/src/app/rules/rules.component.scss b/src/app/rules/rules.component.scss index f6cf3ce..d2eb309 100644 --- a/src/app/rules/rules.component.scss +++ b/src/app/rules/rules.component.scss @@ -32,6 +32,6 @@ mat-form-field { width: 100%; } -.ruleDeleteButton { +.ruleButtons { margin-top: 20px; } diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 8ebee0e..2898275 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -93,9 +93,9 @@ describe('RulesComponent', () => { // Act await page.clickOnCreateNewRule(); fixture.detectChanges(); - await page.setCreateRuleName('New rule'); - await page.setCreateRuleCategory(['Cat1', 'ChildCat1']); - await page.setCreateRuleScript('return fileName === "child_cat_1.txt"'); + await page.setRuleName('New rule'); + await page.setRuleCategory(['Cat1', 'ChildCat1']); + await page.setRuleScript('return fileName === "child_cat_1.txt"'); await page.clickOnCreate(); // Assert @@ -137,6 +137,48 @@ describe('RulesComponent', () => { expect(Page.getRuleNames()) .toEqual([]); })) + + it('should update an existing rule', fakeAsync(async () => { + // Arrange + let ruleService = mockRuleService(); + + let rule: Rule = { + id: 1, + name: 'Rule1', + category: ['Cat1', 'ChildCat1'], + script: 'return fileName === "child_cat_1.txt"' + }; + when(() => ruleService.findAll()).thenResolve([rule]); + + // A refresh is expected after update + let editedRule: Rule = { + id: 1, + name: 'Rule1 edited', + category: ['Cat1', 'ChildCat1'], + script: 'return fileName === "child_cat_1.txt"' + }; + when(() => ruleService.findAll()).thenResolve([editedRule]); + + when(() => ruleService.update(editedRule)).thenResolve(undefined); + + let fixture = MockRender(RulesComponent); + tick(); + + let page = new Page(fixture); + + // Act + await page.clickOnEditFirstRule(); + fixture.detectChanges(); + await page.setRuleName('Rule1 edited'); + await page.clickOnUpdate(); + + // Assert + // No failure in mock setup + tick(); + fixture.detectChanges(); + expect(Page.getRuleNames()) + .toEqual(['Rule1 edited']); + })) }); @@ -195,12 +237,17 @@ class Page { await button.click(); } - async setCreateRuleName(name: string) { + async clickOnEditFirstRule() { + let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Edit'})); + await button.click(); + } + + async setRuleName(name: string) { let input = await this.getInputByFloatingLabel('Name'); await input.setValue(name); } - async setCreateRuleCategory(category: string[]) { + async setRuleCategory(category: string[]) { // let inputHarness = await this.loader.getHarness(MatInputHarness.with({placeholder: 'Select category...'})); let chipGridHarness = await this.loader.getHarness(MatChipGridHarness); let inputHarness = await chipGridHarness.getInput() @@ -213,7 +260,7 @@ class Page { } } - async setCreateRuleScript(script: string) { + async setRuleScript(script: string) { let inputHarness = await this.getInputByFloatingLabel('Script'); await inputHarness.setValue(script); } @@ -223,6 +270,11 @@ class Page { await button.click(); } + async clickOnUpdate() { + let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Update'})); + await button.click(); + } + private async getInputByFloatingLabel(floatingLabelText: string | RegExp) { let formFieldHarness = await this.loader.getHarness(MatFormFieldHarness.with({floatingLabelText: floatingLabelText})); let control = await formFieldHarness.getControl(); diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index a0cf7d5..665a67e 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -15,28 +15,28 @@ export class RulesComponent { readonly separatorKeysCodes = [ENTER] as const; rules: Rule[] = []; - showCreate: boolean = false; - ruleToCreate: Rule = { - name: '', - category: [], - script: '' - }; + ruleToCreateOrUpdate?: Rule = undefined; constructor(private ruleService: RuleService) { this.refresh(); } - createNewRule() { - this.ruleService.create(this.ruleToCreate) - .then(() => { - this.showCreate = false; - this.ruleToCreate = { - name: '', - category: [], - script: '' - }; - this.refresh(); - }) + createOrUpdateRule() { + if (this.ruleToCreateOrUpdate) { + if (!this.ruleToCreateOrUpdate.id) { + this.ruleService.create(this.ruleToCreateOrUpdate) + .then(() => { + this.cancelCreateOrUpdate(); + this.refresh(); + }) + } else { + this.ruleService.update(this.ruleToCreateOrUpdate) + .then(() => { + this.cancelCreateOrUpdate(); + this.refresh(); + }) + } + } } runAll() { @@ -44,7 +44,7 @@ export class RulesComponent { } add(event: MatChipInputEvent) { - this.ruleToCreate.category.push(event.value); + this.ruleToCreateOrUpdate?.category.push(event.value); event.chipInput.clear(); } @@ -55,6 +55,22 @@ export class RulesComponent { }) } + update(rule: Rule) { + this.ruleToCreateOrUpdate = rule; + } + + cancelCreateOrUpdate() { + this.ruleToCreateOrUpdate = undefined; + } + + showCreate() { + this.ruleToCreateOrUpdate = { + name: '', + category: [], + script: '' + }; + } + private refresh() { this.ruleService.findAll() .then(rules => { From 541d28cc7930bc14191891959c92a9efd595cd3a Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 26 Jan 2024 11:40:59 +0100 Subject: [PATCH 44/66] [us40] Run all rules automatically after adding a new rule, a new file or updating an existing rule --- ...atabase-backup-and-restore.service.spec.ts | 2 +- .../database-backup-and-restore.service.ts | 2 +- src/app/rules/rule.repository.ts | 9 + src/app/rules/rule.service.spec.ts | 84 ++++++++- src/app/rules/rule.service.ts | 175 +++++++++++------- src/app/rules/rules.component.spec.ts | 17 +- src/app/rules/rules.component.ts | 5 +- src/app/user-root/user-root.component.spec.ts | 11 +- src/app/user-root/user-root.component.ts | 10 +- 9 files changed, 227 insertions(+), 88 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 153d0ff..ee9ea43 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -165,7 +165,7 @@ describe('DatabaseBackupAndRestoreService', () => { mockFilesCacheService([dbBackupFile]); // Act - let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore(), {defaultValue: undefined}); + let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore()); // Assert tick(); diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 57a7d18..f91db6e 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -50,7 +50,7 @@ export class DatabaseBackupAndRestoreService { finalize(() => this.updateLastDbBackupTime()) ); } else { - return of(); + return of(undefined); } } diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 3bc4da2..610347e 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -2,11 +2,18 @@ import {Injectable} from '@angular/core'; import {db} from "../database/db"; import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; +interface FileRun { + id: string; + value: boolean; +} + export interface Rule { id?: number; name: string; category: string[]; script: string; + // List of all file ids the rule was already run on to avoid running the rule twice in the same condition + fileRuns?: FileRun[]; } @Injectable() @@ -34,6 +41,8 @@ export class RuleRepository { async update(rule: Rule) { if (rule.id) { await db.rules.update(rule.id, rule); + // TODO: Postpone backup until the end of running all rules? Or don't show backup progress? + // Or don't upload it to google drive after each local backup? (and then only show message when uploading to google drive) this.databaseBackupAndRestoreService.backup().subscribe(); } } diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index e5963d1..572a672 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -56,6 +56,11 @@ function mockElectricityBillSample(file: FileElement, fileService: FileService) when(() => ruleRepository.findAll()) .thenResolve(getSampleRules()); + // The first rule should be flagged as matching + let ruleAfterRun = getSampleRules()[0]; + ruleAfterRun.fileRuns = [{id: file.id, value: true}]; + when(() => ruleRepository.update(ruleAfterRun)).thenResolve(); + mockFilesCacheService([file], true); return service; @@ -117,6 +122,41 @@ describe('RuleService', () => { // No unexpected calls to fileService.setCategory })); + it('should not run a rule for a file it was already run on', fakeAsync(async () => { + // Arrange + let backgroundTaskService = mockBackgroundTaskService(); + let progress = mock>(); + when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + .thenReturn(progress); + when(() => progress.next({ + index: 2, + value: 100, + })).thenReturn(); + + let file = mockFileElement('electricity_bill.txt'); + + const service = MockRender(RuleService).point.componentInstance; + + let ruleRepository = ngMocks.findInstance(RuleRepository); + when(() => ruleRepository.findAll()) + .thenResolve([{ + name: 'Electric bill', + category: ['Electricity', 'Bills'], + script: 'return fileContent.startsWith("Electricity Bill");', + fileRuns: [{id: file.id, value: false}] + }]); + + mockFilesCacheService([file]); + + // Act + let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); + + // Assert + tick(); + await runAllPromise; + // No failure in mock setup + })); + it('should automatically categorize a file (using txt file content)', fakeAsync(async () => { // Arrange let backgroundTaskService = mockBackgroundTaskService(); @@ -183,6 +223,31 @@ describe('RuleService', () => { script: 'return false' }]); + // The first rule should be flagged as matching + let ruleAfterRun = { + name: 'Electric bill', + category: ['Electricity', 'Bills'], + script: 'return fileContent.startsWith("Electricity Bill");', + fileRuns: [{id: file.id, value: true}] + }; + when(() => ruleRepository.update(ruleAfterRun)).thenResolve(); + + // Both rules should be flagged as not matching for the other file + ruleAfterRun = { + name: 'Electric bill', + category: ['Electricity', 'Bills'], + script: 'return fileContent.startsWith("Electricity Bill");', + fileRuns: [{id: file.id, value: true}, {id: otherFile.id, value: false}] + }; + when(() => ruleRepository.update(ruleAfterRun)).thenResolve(); + ruleAfterRun = { + name: 'Dumb rule', + category: ['Dumb'], + script: 'return false', + fileRuns: [{id: otherFile.id, value: false}] + }; + when(() => ruleRepository.update(ruleAfterRun)).thenResolve(); + mockFilesCacheService([file, otherFile], true); // Act @@ -225,7 +290,15 @@ describe('RuleService', () => { script: 'return fileContent.includes("test")' }]); - mockFilesCacheService([file], true); + let ruleAfterRun = { + name: 'Dumb file content rule', + category: ['Dumb'], + script: 'return fileContent.includes("test")', + fileRuns: [{id: file.id, value: false}] + }; + when(() => ruleRepository.update(ruleAfterRun)).thenResolve(); + + mockFilesCacheService([file]); // Act let runAllPromise = lastValueFrom(service.runAll(), {defaultValue: undefined}); @@ -283,6 +356,15 @@ describe('RuleService', () => { script: 'return fileContent.startsWith("Dummy");' }]); + // The first rule should be flagged as matching + let ruleAfterRun = { + name: 'Dummy', + category: ['Dummy'], + script: 'return fileContent.startsWith("Dummy");', + fileRuns: [{id: file.id, value: true}] + }; + when(() => ruleRepository.update(ruleAfterRun)).thenResolve(); + mockFilesCacheService([file], true); // Act diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 9be90db..5cb411c 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -1,6 +1,6 @@ import {Injectable} from '@angular/core'; import {FileService} from "../file-list/file.service"; -import {BehaviorSubject, concatMap, filter, find, from, last, map, mergeMap, Observable, of, tap, zip} from "rxjs"; +import {BehaviorSubject, concatMap, filter, find, from, map, mergeMap, Observable, of, tap, zip} from "rxjs"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {Rule, RuleRepository} from "./rule.repository"; import {FilesCacheService} from "../files-cache/files-cache.service"; @@ -34,6 +34,25 @@ export class RuleService { this.worker = new Worker(new URL('./rule.worker', import.meta.url)); const pdfWorkerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`; pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerSrc; + + //TODO: mark each file to know which rules have already been run and when, then every time we load the page, + // we check for all pair of rules/files which have not run or are outdated + } + + private static isRuleRunNeeded(rules: Rule[], file: FileElement) { + for (const rule of rules) { + let previousFileRun = rule.fileRuns?.find(fileRun => fileRun.id === file.id); + if (previousFileRun && previousFileRun.value) { + // We already know the matching rule + return false; + } + if (!previousFileRun) { + // There is a rule we need to run which has not been run before + return true; + } + } + // All rules have already been run + return false; } runAll(): Observable { @@ -41,18 +60,16 @@ export class RuleService { .pipe(mergeMap(rules => { let fileOrFolders = this.filesCacheService.getAll() // Get all files - let files = fileOrFolders.filter(file => isFileElement(file)) - .map(value => value as FileElement); + let files = fileOrFolders + .filter((file): file is FileElement => isFileElement(file)); + // Run the script for each file to get the associated category // The amount of step is one download per file and one per rule running for each file let stepAmount = files.length * (1 + rules.length); let progress = this.backgroundTaskService.showProgress('Running all rules', '', stepAmount); - return this.computeFileToCategoryMap(files, rules, progress) - .pipe(mergeMap(fileToCategoryMap => { - // Set the computed category for each files - return this.setAllFileCategory(fileToCategoryMap); - }), tap({complete: () => progress.next({value: 100, index: stepAmount})})) + return this.runAllAndSetCategories(files, rules, progress) + .pipe(tap({complete: () => progress.next({value: 100, index: stepAmount})})); })); } @@ -73,63 +90,97 @@ export class RuleService { } /** - * Run the given rules on the given files and return the associated category for each file that got a matching rule + * Run the given rules on the given files and set the associated category for each file that got a matching rule */ - private computeFileToCategoryMap(files: FileElement[], rules: Rule[], progress: BehaviorSubject) { - let fileToCategoryMap = new Map(); + private runAllAndSetCategories(files: FileElement[], rules: Rule[], progress: BehaviorSubject) { return zip(from(files) .pipe(concatMap((file, fileIndex) => { let progressIndex = 1 + fileIndex * (rules.length + 1); - - let fileContentObservable: Observable; - if (this.isFileContentReadable(file)) { - progress.next({ - index: progressIndex, - value: 0, - description: "Downloading file content of '" + file.name + "'" - }); - fileContentObservable = this.fileService.downloadFile(file, progress) - .pipe(mergeMap(blobContent => { - if (file.mimeType === 'application/pdf') { - return fromPromise(blobContent.arrayBuffer() - .then(arrayBuffer => pdfjs.getDocument(arrayBuffer).promise) - .then(pdfDocument => pdfDocument.getPage(1)) - .then(firstPage => firstPage.getTextContent()) - .then(textContent => textContent.items - .filter((item): item is TextItem => item !== undefined) - .map(item => "" + item.str).join())); - } else { - return fromPromise(blobContent.text()); - } - })); - } else { - fileContentObservable = of(""); + if (!RuleService.isRuleRunNeeded(rules, file)) { + return of(undefined); } - return fileContentObservable.pipe( + return this.getFileContent(file, progress, progressIndex).pipe( mergeMap(fileContent => { // Find the first rule which matches - return from(rules).pipe(concatMap((rule, ruleIndex) => { - progress.next({ - index: progressIndex + 1 + ruleIndex, - value: 0, - description: "Running rule '" + rule.name + "' for '" + file.name + "'" - }); - return this.run(rule, file, fileContent, progress, progressIndex + 1 + ruleIndex); - }), - // Find will stop running further scripts once we got a match - find(result => { - return result.value; - }), - map(result => { + return this.runAllRules(rules, progress, progressIndex, file, fileContent) + .pipe(mergeMap(result => { + // TODO: do the call to change the category immediately instead of constructing this map + // TODO: How to handle rules that have not run due to finding another matching rule? flag the matching files? if (result) { - fileToCategoryMap.set(file, result.rule.category); + return this.findOrCreateCategories(Object.assign([], result.rule.category), this.filesCacheService.getBaseFolder()) + // There is no need to set the category if the current category is correct + .pipe(filter(categoryId => file.parentId !== categoryId), + mergeMap(categoryId => { + return this.fileService.setCategory(file.id, categoryId); + })); + } else { + return of(); } })); })); }))) - .pipe(last(), - map(() => fileToCategoryMap)); + .pipe(map(() => { + })); + + } + private runAllRules(rulesToRun: Rule[], progress: BehaviorSubject, progressIndex: number, file: FileElement, fileContent: string) { + return from(rulesToRun).pipe(concatMap((rule, ruleIndex) => { + let previousFileRun = rule.fileRuns?.find(fileRun => fileRun.id === file.id); + if (previousFileRun) { + // The rule was run previously, so we already know the result + let result: RuleResult = { + rule: rule, + value: previousFileRun.value + }; + return of(result); + } + progress.next({ + index: progressIndex + 1 + ruleIndex, + value: 0, + description: "Running rule '" + rule.name + "' for '" + file.name + "'" + }); + return this.run(rule, file, fileContent, progress, progressIndex + 1 + ruleIndex) + .pipe(tap((result) => { + // Add this file run to the rule fileRuns to avoid doing the same run again + let rule = result.rule; + if (!rule.fileRuns) { + rule.fileRuns = []; + } + rule.fileRuns.push({id: file.id, value: result.value}); + this.ruleRepository.update(rule); + })); + }), + // Find will stop running further scripts once we got a match + find(result => { + return result.value; + })); + } + + private getFileContent(file: FileElement, progress: BehaviorSubject, progressIndex: number) { + if (this.isFileContentReadable(file)) { + progress.next({ + index: progressIndex, + value: 0, + description: "Downloading file content of '" + file.name + "'" + }); + return this.fileService.downloadFile(file, progress) + .pipe(mergeMap(blobContent => { + if (file.mimeType === 'application/pdf') { + return fromPromise(blobContent.arrayBuffer() + .then(arrayBuffer => pdfjs.getDocument(arrayBuffer).promise) + .then(pdfDocument => pdfDocument.getPage(1)) + .then(firstPage => firstPage.getTextContent()) + .then(textContent => textContent.items + .filter((item): item is TextItem => item !== undefined) + .map(item => "" + item.str).join())); + } else { + return fromPromise(blobContent.text()); + } + })); + } else { + return of(""); + } } private isFileContentReadable(file: FileElement) { @@ -151,27 +202,7 @@ export class RuleService { }); } - /** - * Find or create the categories for each file and associate them - */ - private setAllFileCategory(fileToCategoryMap: Map): Observable { - let baseFolderId = this.filesCacheService.getBaseFolder(); - let categoryRequests: Observable[] = []; - fileToCategoryMap - .forEach((category, file) => { - categoryRequests.push(this.findOrCreateCategories(category, baseFolderId) - // There is no need to set the category if the current category is correct - .pipe(filter(categoryId => file.parentId !== categoryId), - mergeMap(categoryId => { - return this.fileService.setCategory(file.id, categoryId); - }))); - }); - return zip(categoryRequests).pipe(map(() => { - })); - } - // TODO: move and refactor duplicate to FileService - private findOrCreateCategories(categories: string[], categoryId: string): Observable { let categoryName = categories.shift(); if (categoryName !== undefined) { diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 2898275..5b84fb2 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -1,7 +1,7 @@ import {RulesComponent} from './rules.component'; import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; import {AppModule} from "../app.module"; -import {when} from "strong-mock"; +import {mock, when} from "strong-mock"; import {MatButtonHarness} from "@angular/material/button/testing"; import {HarnessLoader, TestKey} from "@angular/cdk/testing"; import {ComponentFixture, fakeAsync, tick} from "@angular/core/testing"; @@ -27,7 +27,10 @@ describe('RulesComponent', () => { .keep(FormsModule) .keep(MatChipsModule) .keep(BreakpointObserver) - .mock(RuleService) + .provide({ + provide: RuleService, + useValue: mock() + }) .replace(BrowserAnimationsModule, NoopAnimationsModule) ); @@ -140,13 +143,14 @@ describe('RulesComponent', () => { it('should update an existing rule', fakeAsync(async () => { // Arrange - let ruleService = mockRuleService(); + let ruleService = ngMocks.get(RuleService); let rule: Rule = { id: 1, name: 'Rule1', category: ['Cat1', 'ChildCat1'], - script: 'return fileName === "child_cat_1.txt"' + script: 'return fileName === "child_cat_1.txt"', + fileRuns: [{id: "1", value: true}] }; when(() => ruleService.findAll()).thenResolve([rule]); @@ -155,13 +159,14 @@ describe('RulesComponent', () => { id: 1, name: 'Rule1 edited', category: ['Cat1', 'ChildCat1'], - script: 'return fileName === "child_cat_1.txt"' + script: 'return fileName === "child_cat_1.txt"', + fileRuns: [] }; when(() => ruleService.findAll()).thenResolve([editedRule]); when(() => ruleService.update(editedRule)).thenResolve(undefined); - let fixture = MockRender(RulesComponent); + let fixture = MockRender(RulesComponent, null, {reset: true}); tick(); let page = new Page(fixture); diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 665a67e..38ad30b 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -8,8 +8,7 @@ import {Rule} from "./rule.repository"; @Component({ selector: 'app-rules', templateUrl: './rules.component.html', - styleUrls: ['./rules.component.scss'], - providers: [RuleService] + styleUrls: ['./rules.component.scss'] }) export class RulesComponent { readonly separatorKeysCodes = [ENTER] as const; @@ -30,6 +29,8 @@ export class RulesComponent { this.refresh(); }) } else { + // Reset fileRuns to ensure we will automatically re-run the rule + this.ruleToCreateOrUpdate.fileRuns = []; this.ruleService.update(this.ruleToCreateOrUpdate) .then(() => { this.cancelCreateOrUpdate(); diff --git a/src/app/user-root/user-root.component.spec.ts b/src/app/user-root/user-root.component.spec.ts index 6325a53..a0ceace 100644 --- a/src/app/user-root/user-root.component.spec.ts +++ b/src/app/user-root/user-root.component.spec.ts @@ -6,18 +6,25 @@ import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-functi import {mockDatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service.spec"; import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; import {fakeAsync, tick} from "@angular/core/testing"; +import {RuleService} from "../rules/rule.service"; +import {mockRuleService} from "../rules/rule.service.spec"; describe('UserRootComponent', () => { beforeEach(() => MockBuilder(UserRootComponent, AppModule) .mock(DatabaseBackupAndRestoreService) + .mock(RuleService) ) - it('should restore automatically', fakeAsync(() => { + it('should restore and run all rules automatically', fakeAsync(() => { // Arrange let databaseBackupAndRestoreService = mockDatabaseBackupAndRestoreService(); + let restoreObservable = mustBeConsumedAsyncObservable(undefined); when(() => databaseBackupAndRestoreService.restore()) - .thenReturn(mustBeConsumedAsyncObservable(undefined)); + .thenReturn(restoreObservable); + let ruleService = mockRuleService(); + when(() => ruleService.runAll()) + .thenReturn(mustBeConsumedAsyncObservable(undefined, restoreObservable)) // Act MockRender(UserRootComponent); diff --git a/src/app/user-root/user-root.component.ts b/src/app/user-root/user-root.component.ts index 77b3950..449754b 100644 --- a/src/app/user-root/user-root.component.ts +++ b/src/app/user-root/user-root.component.ts @@ -3,15 +3,19 @@ import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-r import {RuleRepository} from "../rules/rule.repository"; import {FileUploadService} from "../file-upload/file-upload.service"; import {FilesCacheService} from "../files-cache/files-cache.service"; +import {RuleService} from "../rules/rule.service"; @Component({ selector: 'app-user-root', templateUrl: './user-root.component.html', styleUrls: ['./user-root.component.scss'], - providers: [RuleRepository, DatabaseBackupAndRestoreService, FileUploadService, FilesCacheService] + providers: [RuleRepository, RuleService, DatabaseBackupAndRestoreService, FileUploadService, FilesCacheService] }) export class UserRootComponent { - constructor(databaseBackupAndRestoreService: DatabaseBackupAndRestoreService) { - databaseBackupAndRestoreService.restore().subscribe(); + constructor(databaseBackupAndRestoreService: DatabaseBackupAndRestoreService, ruleService: RuleService) { + databaseBackupAndRestoreService.restore() + .subscribe(() => { + ruleService.runAll().subscribe(); + }); } } From 8152a4c0c7dca9f6e2970cc514bc0e6b6cfdf82f Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 26 Jan 2024 11:58:23 +0100 Subject: [PATCH 45/66] [us40] Remove now unneeded runAll button on rules page --- src/app/rules/rules.component.html | 2 -- src/app/rules/rules.component.spec.ts | 46 +++++++-------------------- src/app/rules/rules.component.ts | 4 --- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 2f89319..875ab47 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -6,8 +6,6 @@

Setup rules

-   -
diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 5b84fb2..1d1f80f 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -6,8 +6,6 @@ import {MatButtonHarness} from "@angular/material/button/testing"; import {HarnessLoader, TestKey} from "@angular/cdk/testing"; import {ComponentFixture, fakeAsync, tick} from "@angular/core/testing"; import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; -import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; -import {mockRuleService} from "./rule.service.spec"; import {MatInputHarness} from "@angular/material/input/testing"; import {MatFormFieldHarness} from "@angular/material/form-field/testing"; import {MatFormFieldModule} from "@angular/material/form-field"; @@ -39,7 +37,7 @@ describe('RulesComponent', () => { mockSampleRules() // Act - let component = MockRender(RulesComponent).point.componentInstance; + let component = MockRender(RulesComponent, null, {reset: true}).point.componentInstance; // Assert expect(component).toBeTruthy(); @@ -50,7 +48,7 @@ describe('RulesComponent', () => { mockSampleRules(); // Act - let fixture = MockRender(RulesComponent); + let fixture = MockRender(RulesComponent, null, {reset: true}); // Assert tick(); @@ -60,24 +58,9 @@ describe('RulesComponent', () => { expect(Page.getRuleScript('Electric bill')).toEqual('return fileName === "electricity_bill.pdf"'); })) - it('should run all the rules when clicking on "run rules" button', async () => { - // Arrange - mockSampleRules(); - let ruleService = mockRuleService(); - when(() => ruleService.runAll()).thenReturn(mustBeConsumedAsyncObservable(undefined)); - let fixture = MockRender(RulesComponent); - let page = new Page(fixture); - - // Act - await page.clickOnRunRulesButton(); - - // Assert - // no failure from mock setup - }) - it('should create a new rule', fakeAsync(async () => { // Arrange - let ruleService = mockRuleService(); + let ruleService = ngMocks.get(RuleService); when(() => ruleService.findAll()).thenResolve([]); let expectedRule: Rule = { @@ -89,7 +72,7 @@ describe('RulesComponent', () => { // After refresh, there should be the new rule when(() => ruleService.findAll()).thenResolve([expectedRule]); - let fixture = MockRender(RulesComponent); + let fixture = MockRender(RulesComponent, null, {reset: true}); let page = new Page(fixture); @@ -111,7 +94,7 @@ describe('RulesComponent', () => { it('should delete an existing rule', fakeAsync(async () => { // Arrange - let ruleService = mockRuleService(); + let ruleService = ngMocks.get(RuleService); let rule: Rule = { name: 'Rule1', @@ -125,7 +108,7 @@ describe('RulesComponent', () => { when(() => ruleService.delete(rule)).thenResolve(); - let fixture = MockRender(RulesComponent); + let fixture = MockRender(RulesComponent, null, {reset: true}); tick(); let page = new Page(fixture); @@ -188,7 +171,7 @@ describe('RulesComponent', () => { function mockSampleRules() { - let ruleService = mockRuleService(); + let ruleService = ngMocks.get(RuleService); when(() => ruleService.findAll()).thenResolve(getSampleRules()); } @@ -232,11 +215,6 @@ class Page { .nativeNode.textContent.trim(); } - async clickOnRunRulesButton() { - let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Run all'})); - return button.click(); - } - async clickOnCreateNewRule() { let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Create new rule'})); await button.click(); @@ -280,6 +258,11 @@ class Page { await button.click(); } + async deleteFirstRule() { + let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Delete'})); + await button.click(); + } + private async getInputByFloatingLabel(floatingLabelText: string | RegExp) { let formFieldHarness = await this.loader.getHarness(MatFormFieldHarness.with({floatingLabelText: floatingLabelText})); let control = await formFieldHarness.getControl(); @@ -288,9 +271,4 @@ class Page { } throw Error("No input found with floating label '" + floatingLabelText + "'"); } - - async deleteFirstRule() { - let button = await this.loader.getHarness(MatButtonHarness.with({text: 'Delete'})); - await button.click(); - } } diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 38ad30b..28c90f9 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -40,10 +40,6 @@ export class RulesComponent { } } - runAll() { - this.ruleService.runAll().subscribe(); - } - add(event: MatChipInputEvent) { this.ruleToCreateOrUpdate?.category.push(event.value); event.chipInput.clear(); From 02795f5104194c0e94317e3b81a859199a738bf1 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 26 Jan 2024 14:19:01 +0100 Subject: [PATCH 46/66] [us40] Do not display any toast message (quick dismiss) if there were no actual steps to inform the users about when doing background tasks --- .../background-task.service.spec.ts | 26 ++++++++++++++++--- .../background-task.service.ts | 14 +++++++--- ...atabase-backup-and-restore.service.spec.ts | 6 ++--- .../database-backup-and-restore.service.ts | 6 ++--- src/app/rules/rule.service.spec.ts | 10 +++---- src/app/rules/rule.service.ts | 2 +- 6 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 7a2a6a1..2ba6949 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -29,7 +29,7 @@ describe('BackgroundTaskService', () => { const backgroundTaskService = fixture.point.componentInstance; // Act - backgroundTaskService.showProgress("Test", "Doing first test", 2); + backgroundTaskService.showProgress("Test", 2, "Doing first test"); // Assert let result = await Page.getProgressMessage(); @@ -40,7 +40,7 @@ describe('BackgroundTaskService', () => { // Arrange let fixture = MockRender(BackgroundTaskService); const backgroundTaskService = fixture.point.componentInstance; - let progress = backgroundTaskService.showProgress("Test", "Doing first test", 4); + let progress = backgroundTaskService.showProgress("Test", 4, "Doing first test"); // Act progress.next({ @@ -60,7 +60,7 @@ describe('BackgroundTaskService', () => { let fixture = MockRender(BackgroundTaskService); let page = new Page(); const backgroundTaskService = fixture.point.componentInstance; - let progress = backgroundTaskService.showProgress("Test", "Doing first test", 2); + let progress = backgroundTaskService.showProgress("Test", 2, "Doing first test"); // Act progress.next({ @@ -70,13 +70,33 @@ describe('BackgroundTaskService', () => { // Assert fixture.detectChanges(); + tick(); let resultMessage = await Page.getProgressMessage(); expect(resultMessage).toEqual("2/2 100% Test finished!"); tick(3000); // The message should be gone after 5 seconds at least resultMessage = await Page.getProgressMessage(); expect(resultMessage).toEqual(undefined); + })) + + it('Should dismiss immediately if it finished with no actual step', fakeAsync(async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + let page = new Page(); + const backgroundTaskService = fixture.point.componentInstance; + let progress = backgroundTaskService.showProgress("Test", 2); + + // Act + progress.next({ + index: 2, + value: 100 + }) + // Assert + fixture.detectChanges(); + tick(); + let resultMessage = await Page.getProgressMessage(); + expect(resultMessage).toEqual(undefined); })) }) describe('updateProgress', () => { diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index e769ace..1edc3b8 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -12,7 +12,7 @@ export class BackgroundTaskService { constructor(private snackBar: MatSnackBar) { } - showProgress(globalDescription: string, stepDescription: string, stepAmount: number): BehaviorSubject { + showProgress(globalDescription: string, stepAmount: number, stepDescription?: string): BehaviorSubject { let progress = new BehaviorSubject({ index: 1, value: 0, @@ -82,13 +82,21 @@ export interface Progress { class SnackBarProgressIndicatorComponent { progress: Progress; - constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ProgressData, private snackBarRef: MatSnackBarRef) { + constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ProgressData, snackBarRef: MatSnackBarRef) { this.progress = data.progress.getValue(); data.progress.subscribe(progress => { + let noStepYet = !this.progress.description; this.progress = progress; if (this.isFinished()) { - snackBarRef._dismissAfter(3000); + if (noStepYet) { + // There was no step, and it's already finished, + // we can simply dismiss the message since there is actually nothing to inform the users about + snackBarRef.dismiss(); + } else { + // The user must see that something happened + snackBarRef._dismissAfter(3000); + } } }) } diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index ee9ea43..40ab810 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -25,7 +25,7 @@ function setupMockForRestore(dbBackupFile: FileElement) { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Automatic restore", "Downloading last backup", 2)) + when(() => backgroundTaskService.showProgress("Automatic restore", 2, "Downloading last backup")) .thenReturn(progress); when(() => progress.next({index: 2, value: 0, description: "Importing last backup"})).thenReturn(); when(() => progress.next({index: 2, value: 100})).thenReturn(); @@ -184,7 +184,7 @@ describe('DatabaseBackupAndRestoreService', () => { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) + when(() => backgroundTaskService.showProgress("Backup", 2, "Creating backup")) .thenReturn(progress); when(() => progress.next({index: 2, description: "Uploading backup", value: 0})).thenReturn(); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); @@ -214,7 +214,7 @@ describe('DatabaseBackupAndRestoreService', () => { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Backup", "Creating backup", 2)) + when(() => backgroundTaskService.showProgress("Backup", 2, "Creating backup")) .thenReturn(progress); when(() => progress.next({index: 2, description: "Uploading backup", value: 0})).thenReturn(); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index f91db6e..5a08a18 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -20,8 +20,7 @@ export class DatabaseBackupAndRestoreService { } backup() { - let progress = this.backgroundTaskService.showProgress('Backup', - "Creating backup", 2); + let progress = this.backgroundTaskService.showProgress('Backup', 2, "Creating backup"); return from(exportDB(db)) .pipe(tap(() => progress.next({index: 2, value: 0, description: "Uploading backup"})), mergeMap(blob => { @@ -37,8 +36,7 @@ export class DatabaseBackupAndRestoreService { let lastDbBackupTime = this.getLastDbBackupTime(); let modifiedTime = dbFile?.modifiedTime ?? Date.now(); if (dbFile && modifiedTime > lastDbBackupTime) { - let progress = this.backgroundTaskService.showProgress('Automatic restore', - "Downloading last backup", 2); + let progress = this.backgroundTaskService.showProgress('Automatic restore', 2, "Downloading last backup"); return this.fileService.downloadFile(dbFile, progress) .pipe( tap(() => progress.next({index: 2, value: 0, description: 'Importing last backup'})), diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 572a672..fde9903 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -28,7 +28,7 @@ function mockBillCategoryFindOrCreate(fileService: FileService) { function mockElectricityBillSample(file: FileElement, fileService: FileService) { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Running all rules", "", 3)) + when(() => backgroundTaskService.showProgress("Running all rules", 3)) .thenReturn(progress); when(() => progress.next({ index: 1, @@ -126,7 +126,7 @@ describe('RuleService', () => { // Arrange let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + when(() => backgroundTaskService.showProgress("Running all rules", 2)) .thenReturn(progress); when(() => progress.next({ index: 2, @@ -161,7 +161,7 @@ describe('RuleService', () => { // Arrange let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Running all rules", "", 6)) + when(() => backgroundTaskService.showProgress("Running all rules", 6)) .thenReturn(progress); when(() => progress.next({ index: 1, @@ -263,7 +263,7 @@ describe('RuleService', () => { // Arrange let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + when(() => backgroundTaskService.showProgress("Running all rules", 2)) .thenReturn(progress); when(() => progress.next({ index: 2, @@ -313,7 +313,7 @@ describe('RuleService', () => { // Arrange let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Running all rules", "", 2)) + when(() => backgroundTaskService.showProgress("Running all rules", 2)) .thenReturn(progress); when(() => progress.next({ index: 1, diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 5cb411c..7de49ad 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -67,7 +67,7 @@ export class RuleService { // Run the script for each file to get the associated category // The amount of step is one download per file and one per rule running for each file let stepAmount = files.length * (1 + rules.length); - let progress = this.backgroundTaskService.showProgress('Running all rules', '', stepAmount); + let progress = this.backgroundTaskService.showProgress('Running all rules', stepAmount); return this.runAllAndSetCategories(files, rules, progress) .pipe(tap({complete: () => progress.next({value: 100, index: stepAmount})})); })); From 45232300994ebcfbf6be2420f5e7c3c26b38a36a Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 26 Jan 2024 14:57:18 +0100 Subject: [PATCH 47/66] [us40] Delay backup calls after rule update to avoid doing too many backups in a short amount of time when running all rules --- ...atabase-backup-and-restore.service.spec.ts | 43 +++++++++++++++++++ .../database-backup-and-restore.service.ts | 13 ++++++ src/app/rules/rule.repository.spec.ts | 7 ++- src/app/rules/rule.repository.ts | 4 +- 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index 40ab810..f6ccc01 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -238,6 +238,49 @@ describe('DatabaseBackupAndRestoreService', () => { await backupPromise; }); }) + + describe('scheduleBackup', () => { + it('should only do one backup after multiple calls', fakeAsync(() => { + // Arrange + const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + // Mock the backup call since we already test it above + databaseBackupAndRestoreService.backup = mock(); + + when(() => databaseBackupAndRestoreService.backup()) + .thenReturn(mustBeConsumedAsyncObservable({type: HttpEventType.Response} as HttpResponse)) + .times(1); + + // Act + databaseBackupAndRestoreService.scheduleBackup(); + databaseBackupAndRestoreService.scheduleBackup(); + + // Assert + // No failure with mockup setup + tick(5000); + })); + + it('should do a second backup after the previous backup finished', fakeAsync(() => { + // Arrange + const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; + // Mock the backup call since we already test it above + databaseBackupAndRestoreService.backup = mock(); + + when(() => databaseBackupAndRestoreService.backup()) + .thenReturn(mustBeConsumedAsyncObservable({type: HttpEventType.Response} as HttpResponse)) + .times(2); + + // Do a first backup and wait 30s + databaseBackupAndRestoreService.scheduleBackup(); + tick(30_000); + + // Act + databaseBackupAndRestoreService.scheduleBackup(); + + // Assert + // No failure with mockup setup + tick(5000); + })); + }) }); export function mockDatabaseBackupAndRestoreService() { diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index 5a08a18..e41c5d2 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -14,6 +14,8 @@ export class DatabaseBackupAndRestoreService { private static readonly LAST_DB_BACKUP_TIME = 'last_db_backup_time'; private static readonly DB_NAME = 'db.backup'; + private static readonly BACKUP_DELAY = 5_000; + private backupScheduled = false; constructor(private fileUploadService: FileUploadService, private fileService: FileService, private backgroundTaskService: BackgroundTaskService, private filesCacheService: FilesCacheService) { @@ -31,6 +33,17 @@ export class DatabaseBackupAndRestoreService { finalize(() => this.updateLastDbBackupTime())); } + scheduleBackup() { + if (!this.backupScheduled) { + this.backupScheduled = true; + + setTimeout(() => { + this.backupScheduled = false; + this.backup().subscribe(); + }, DatabaseBackupAndRestoreService.BACKUP_DELAY); + } + } + restore(): Observable { let dbFile = this.findExistingDbFile(); let lastDbBackupTime = this.getLastDbBackupTime(); diff --git a/src/app/rules/rule.repository.spec.ts b/src/app/rules/rule.repository.spec.ts index 2cb772e..329baf2 100644 --- a/src/app/rules/rule.repository.spec.ts +++ b/src/app/rules/rule.repository.spec.ts @@ -125,8 +125,11 @@ describe('RuleRepository', () => { it('should update an existing rule', async () => { // Arrange const ruleRepository = MockRender(RuleRepository).point.componentInstance; - // 2 calls to 'backup' expected, from create, and then from update - mockBackupCall().times(2); + // 2 calls to 'backup' then 'scheduleBackup' expected, from create, and then from update + let databaseBackupAndRestoreService = ngMocks.get(DatabaseBackupAndRestoreService); + mockBackupCall(); + when(() => databaseBackupAndRestoreService.scheduleBackup()).thenReturn(); + let rule: Rule = { name: 'TestRule', category: ['Test1', 'ChildTest1'], diff --git a/src/app/rules/rule.repository.ts b/src/app/rules/rule.repository.ts index 610347e..ea4e13e 100644 --- a/src/app/rules/rule.repository.ts +++ b/src/app/rules/rule.repository.ts @@ -41,9 +41,7 @@ export class RuleRepository { async update(rule: Rule) { if (rule.id) { await db.rules.update(rule.id, rule); - // TODO: Postpone backup until the end of running all rules? Or don't show backup progress? - // Or don't upload it to google drive after each local backup? (and then only show message when uploading to google drive) - this.databaseBackupAndRestoreService.backup().subscribe(); + this.databaseBackupAndRestoreService.scheduleBackup(); } } } From 04b72b014e66e5dd059dac42ebfc455bc90db64e Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 26 Jan 2024 15:28:03 +0100 Subject: [PATCH 48/66] [us40] Immediately reload the page and run all rules after a rule CUD --- .../database-backup-and-restore.service.ts | 8 +++--- .../files-cache/files-cache.service.spec.ts | 25 ++++++++++++++++++- src/app/rules/rules.component.spec.ts | 25 ++++++++++++------- src/app/rules/rules.component.ts | 14 ++++++----- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index e41c5d2..aa2624c 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -15,7 +15,7 @@ export class DatabaseBackupAndRestoreService { private static readonly LAST_DB_BACKUP_TIME = 'last_db_backup_time'; private static readonly DB_NAME = 'db.backup'; private static readonly BACKUP_DELAY = 5_000; - private backupScheduled = false; + private static backupScheduled = false; constructor(private fileUploadService: FileUploadService, private fileService: FileService, private backgroundTaskService: BackgroundTaskService, private filesCacheService: FilesCacheService) { @@ -34,11 +34,11 @@ export class DatabaseBackupAndRestoreService { } scheduleBackup() { - if (!this.backupScheduled) { - this.backupScheduled = true; + if (!DatabaseBackupAndRestoreService.backupScheduled) { + DatabaseBackupAndRestoreService.backupScheduled = true; setTimeout(() => { - this.backupScheduled = false; + DatabaseBackupAndRestoreService.backupScheduled = false; this.backup().subscribe(); }, DatabaseBackupAndRestoreService.BACKUP_DELAY); } diff --git a/src/app/files-cache/files-cache.service.spec.ts b/src/app/files-cache/files-cache.service.spec.ts index 0ac81c9..a8cc5e6 100644 --- a/src/app/files-cache/files-cache.service.spec.ts +++ b/src/app/files-cache/files-cache.service.spec.ts @@ -4,11 +4,15 @@ import {MockBuilder, MockRender, ngMocks} from "ng-mocks"; import {mock, UnexpectedProperty, when} from "strong-mock"; import {AppModule} from "../app.module"; import {mockFileElement} from "../file-list/file-list.component.spec"; -import {ActivatedRoute, ActivatedRouteSnapshot, Data} from "@angular/router"; +import {ActivatedRoute, ActivatedRouteSnapshot, Data, Router} from "@angular/router"; import {FilesCache} from "./files.resolver"; describe('FilesCacheService', () => { beforeEach(() => MockBuilder(FilesCacheService, AppModule) + .provide({ + provide: Router, + useValue: mock() + }) .provide({ provide: ActivatedRoute, useValue: mock({ @@ -69,6 +73,25 @@ describe('FilesCacheService', () => { }) }) + describe('refreshCacheAndReload', () => { + it('should refresh cache and reload', () => { + // Arrange + const service = MockRender(FilesCacheService).point.componentInstance; + + let router = ngMocks.get(Router); + when(() => router.url).thenReturn("currentUrl") + when(() => router.navigate(['currentUrl'], {onSameUrlNavigation: "reload"})) + .thenResolve(true); + + // Act + service.refreshCacheAndReload(); + + // Assert + expect(FilesCacheService.reloadRouteData).toBeTruthy(); + }) + + }) + }); export function mockFilesCacheServiceGetBaseFolder() { diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 1d1f80f..e348212 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -17,6 +17,14 @@ import {MatChipGridHarness} from "@angular/material/chips/testing"; import {Rule} from "./rule.repository"; import {BreakpointObserver} from "@angular/cdk/layout"; import {RuleService} from "./rule.service"; +import {Router} from "@angular/router"; + +function mockRouterReloadPage() { + let router = ngMocks.get(Router); + when(() => router.url).thenReturn("currentUrl") + when(() => router.navigate(['currentUrl'], {onSameUrlNavigation: "reload"})) + .thenResolve(true); +} describe('RulesComponent', () => { beforeEach(() => MockBuilder(RulesComponent, AppModule) @@ -25,6 +33,10 @@ describe('RulesComponent', () => { .keep(FormsModule) .keep(MatChipsModule) .keep(BreakpointObserver) + .provide({ + provide: Router, + useValue: mock() + }) .provide({ provide: RuleService, useValue: mock() @@ -71,7 +83,7 @@ describe('RulesComponent', () => { when(() => ruleService.create(expectedRule)).thenResolve(undefined); // After refresh, there should be the new rule - when(() => ruleService.findAll()).thenResolve([expectedRule]); + mockRouterReloadPage(); let fixture = MockRender(RulesComponent, null, {reset: true}); let page = new Page(fixture); @@ -88,8 +100,6 @@ describe('RulesComponent', () => { // No failure in mock setup tick(); fixture.detectChanges(); - expect(Page.getRuleNames()) - .toEqual(['New rule']); })) it('should delete an existing rule', fakeAsync(async () => { @@ -104,7 +114,7 @@ describe('RulesComponent', () => { when(() => ruleService.findAll()).thenResolve([rule]); // A refresh is expected after delete - when(() => ruleService.findAll()).thenResolve([]); + mockRouterReloadPage(); when(() => ruleService.delete(rule)).thenResolve(); @@ -120,8 +130,6 @@ describe('RulesComponent', () => { // No failure in mock setup tick(); fixture.detectChanges(); - expect(Page.getRuleNames()) - .toEqual([]); })) it('should update an existing rule', fakeAsync(async () => { @@ -138,6 +146,8 @@ describe('RulesComponent', () => { when(() => ruleService.findAll()).thenResolve([rule]); // A refresh is expected after update + mockRouterReloadPage(); + let editedRule: Rule = { id: 1, name: 'Rule1 edited', @@ -145,7 +155,6 @@ describe('RulesComponent', () => { script: 'return fileName === "child_cat_1.txt"', fileRuns: [] }; - when(() => ruleService.findAll()).thenResolve([editedRule]); when(() => ruleService.update(editedRule)).thenResolve(undefined); @@ -164,8 +173,6 @@ describe('RulesComponent', () => { // No failure in mock setup tick(); fixture.detectChanges(); - expect(Page.getRuleNames()) - .toEqual(['Rule1 edited']); })) }); diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 28c90f9..8d4d0e1 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -3,6 +3,7 @@ import {RuleService} from "./rule.service"; import {ENTER} from "@angular/cdk/keycodes"; import {MatChipInputEvent} from "@angular/material/chips"; import {Rule} from "./rule.repository"; +import {Router} from "@angular/router"; @Component({ @@ -16,8 +17,11 @@ export class RulesComponent { rules: Rule[] = []; ruleToCreateOrUpdate?: Rule = undefined; - constructor(private ruleService: RuleService) { - this.refresh(); + constructor(private ruleService: RuleService, private router: Router) { + this.ruleService.findAll() + .then(rules => { + this.rules = rules; + }) } createOrUpdateRule() { @@ -69,9 +73,7 @@ export class RulesComponent { } private refresh() { - this.ruleService.findAll() - .then(rules => { - this.rules = rules; - }) + // Reload page + this.router.navigate([this.router.url], {onSameUrlNavigation: "reload"}); } } From 35ee48263327f5b046bb8b70bcce31508747c454 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 26 Jan 2024 15:30:02 +0100 Subject: [PATCH 49/66] [us40] Remove already implemented TODO in ruleService --- src/app/rules/rule.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 7de49ad..13aefe6 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -34,9 +34,6 @@ export class RuleService { this.worker = new Worker(new URL('./rule.worker', import.meta.url)); const pdfWorkerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`; pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerSrc; - - //TODO: mark each file to know which rules have already been run and when, then every time we load the page, - // we check for all pair of rules/files which have not run or are outdated } private static isRuleRunNeeded(rules: Rule[], file: FileElement) { @@ -104,8 +101,6 @@ export class RuleService { // Find the first rule which matches return this.runAllRules(rules, progress, progressIndex, file, fileContent) .pipe(mergeMap(result => { - // TODO: do the call to change the category immediately instead of constructing this map - // TODO: How to handle rules that have not run due to finding another matching rule? flag the matching files? if (result) { return this.findOrCreateCategories(Object.assign([], result.rule.category), this.filesCacheService.getBaseFolder()) // There is no need to set the category if the current category is correct From 4973288fcb331663b31d9562ffbbb4252290b0c5 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 1 Feb 2024 15:49:12 +0100 Subject: [PATCH 50/66] [us40] Use ace editor when editing the script --- angular.json | 12 ++- package-lock.json | 10 ++- package.json | 3 +- src/app/ace-editor/ace-editor.component.html | 1 + src/app/ace-editor/ace-editor.component.scss | 4 + .../ace-editor/ace-editor.component.spec.ts | 44 ++++++++++ src/app/ace-editor/ace-editor.component.ts | 84 +++++++++++++++++++ src/app/app.module.ts | 4 +- src/app/file-list/file-list.component.spec.ts | 3 +- src/app/rules/rules.component.html | 4 +- src/app/rules/rules.component.spec.ts | 10 ++- src/app/rules/rules.component.ts | 2 +- 12 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 src/app/ace-editor/ace-editor.component.html create mode 100644 src/app/ace-editor/ace-editor.component.scss create mode 100644 src/app/ace-editor/ace-editor.component.spec.ts create mode 100644 src/app/ace-editor/ace-editor.component.ts diff --git a/angular.json b/angular.json index 70b15bf..3922ba5 100644 --- a/angular.json +++ b/angular.json @@ -27,7 +27,17 @@ "inlineStyleLanguage": "scss", "assets": [ "src/smd_icon.png", - "src/assets" + "src/assets", + { + "glob": "mode-javascript.js", + "input": "node_modules/ace-builds/src-noconflict", + "output": "ace" + }, + { + "glob": "worker-javascript.js", + "input": "node_modules/ace-builds/src-noconflict", + "output": "ace" + } ], "styles": [ "src/theme.scss", diff --git a/package-lock.json b/package-lock.json index 732067c..0fefe8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "storemydocs", - "version": "0.3.1-44-gd69fd7b", + "version": "0.3.1-52-g35ee482", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "storemydocs", - "version": "0.3.1-44-gd69fd7b", + "version": "0.3.1-52-g35ee482", "dependencies": { "@angular/animations": "^17.1.1", "@angular/cdk": "^17.1.0", @@ -19,6 +19,7 @@ "@angular/platform-browser-dynamic": "^17.1.1", "@angular/router": "^17.1.1", "@auth0/angular-jwt": "^5.2.0", + "ace-builds": "^1.32.3", "dexie": "^3.2.4", "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", @@ -4703,6 +4704,11 @@ "node": ">= 0.6" } }, + "node_modules/ace-builds": { + "version": "1.32.3", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.32.3.tgz", + "integrity": "sha512-ptSTUmDEU+LuwGiPY3/qQPmmAWE27vuv5sASL8swLRyLGJb7Ye7a8MrJ4NnAkFh1sJgVUqKTEGWRRFDmqYPw2Q==" + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", diff --git a/package.json b/package.json index 2647af8..667b047 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "storemydocs", - "version": "0.3.1-44-gd69fd7b", + "version": "0.3.1-52-g35ee482", "scripts": { "ng": "ng", "start": "ng serve", @@ -29,6 +29,7 @@ "@angular/platform-browser-dynamic": "^17.1.1", "@angular/router": "^17.1.1", "@auth0/angular-jwt": "^5.2.0", + "ace-builds": "^1.32.3", "dexie": "^3.2.4", "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", diff --git a/src/app/ace-editor/ace-editor.component.html b/src/app/ace-editor/ace-editor.component.html new file mode 100644 index 0000000..421df16 --- /dev/null +++ b/src/app/ace-editor/ace-editor.component.html @@ -0,0 +1 @@ +
diff --git a/src/app/ace-editor/ace-editor.component.scss b/src/app/ace-editor/ace-editor.component.scss new file mode 100644 index 0000000..29669cc --- /dev/null +++ b/src/app/ace-editor/ace-editor.component.scss @@ -0,0 +1,4 @@ + +.ruleScriptEdit { + height: 200px; +} diff --git a/src/app/ace-editor/ace-editor.component.spec.ts b/src/app/ace-editor/ace-editor.component.spec.ts new file mode 100644 index 0000000..e960c1a --- /dev/null +++ b/src/app/ace-editor/ace-editor.component.spec.ts @@ -0,0 +1,44 @@ +import {AceEditorComponent} from './ace-editor.component'; +import {MockBuilder, MockRender} from "ng-mocks"; +import {AppModule} from "../app.module"; +import {BehaviorSubject} from "rxjs"; +import {fakeAsync, flush} from "@angular/core/testing"; + +describe('AceEditorComponent', () => { + beforeEach(() => MockBuilder(AceEditorComponent, AppModule)); + + it('should create', () => { + let component = MockRender(AceEditorComponent).point.componentInstance; + expect(component).toBeTruthy(); + }); + + it('should set the input', () => { + // Arrange + let params = { + value: "initialValue" + }; + + // Act + let component = MockRender(AceEditorComponent, params).point.componentInstance; + + // Assert + expect(component.editor?.getValue()).toEqual("initialValue") + }); + + it('should get the new output', fakeAsync(() => { + // Arrange + let output = new BehaviorSubject(""); + let params = { + value: "oldValue", + valueChange: output + }; + let component = MockRender(AceEditorComponent, params).point.componentInstance; + + // Act + component.editor?.setValue("newValue"); + + // Assert + flush(); + expect(output.getValue()).toEqual("newValue") + })); +}); diff --git a/src/app/ace-editor/ace-editor.component.ts b/src/app/ace-editor/ace-editor.component.ts new file mode 100644 index 0000000..ec6e3a4 --- /dev/null +++ b/src/app/ace-editor/ace-editor.component.ts @@ -0,0 +1,84 @@ +import {Component, ElementRef, EventEmitter, HostBinding, Input, OnInit, Output, ViewChild} from '@angular/core'; +import * as ace from "ace-builds"; +import {Ace} from "ace-builds"; +import {MatFormFieldControl} from "@angular/material/form-field"; +import {Subject} from "rxjs"; +import Editor = Ace.Editor; + +@Component({ + selector: 'app-ace-editor', + templateUrl: './ace-editor.component.html', + styleUrl: './ace-editor.component.scss', + providers: [{provide: MatFormFieldControl, useExisting: AceEditorComponent}], +}) +export class AceEditorComponent implements MatFormFieldControl, OnInit { + static nextId = 0; + @HostBinding() id = `app-ace-editor-${AceEditorComponent.nextId++}`; + + stateChanges = new Subject(); + ngControl = null; + + @Input() + value!: string; + @Output() valueChange = new EventEmitter(); + + placeholder: string = ""; + focused: boolean = false; + empty: boolean = false; + shouldLabelFloat: boolean = true; + disabled: boolean = false; + errorState: boolean = false; + controlType?: string | undefined = "app-ace-editor"; + autofilled?: boolean | undefined; + userAriaDescribedBy?: string | undefined; + + editor?: Editor; + @ViewChild("ruleScriptEdit", {static: true}) private ruleScriptEdit?: ElementRef; + + constructor() { + ace.config.set("fontSize", "14px"); + ace.config.set("basePath", "ace"); + // TODO: add autocompletion + } + + private _required = false; + + @Input() + get required() { + return this._required; + } + + set required(req: boolean) { + this._required = req; + this.stateChanges.next(); + } + + + ngOnInit() { + if (this.ruleScriptEdit) { + this.editor = ace.edit(this.ruleScriptEdit.nativeElement); + this.editor.session.setMode("ace/mode/javascript"); + + this.editor.setValue(this.value); + this.editor.on("input", () => { + this.valueChange.emit(this.editor?.getValue()); + this.stateChanges.next(); + }); + this.editor.on("focus", () => { + this.focused = true; + this.stateChanges.next(); + }); + this.editor.on("blur", () => { + this.focused = false; + this.stateChanges.next(); + }); + } + } + + setDescribedByIds(ids: string[]): void { + } + + onContainerClick(event: MouseEvent): void { + this.editor?.focus(); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1aa4075..7f86c3c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -36,6 +36,7 @@ import {MatExpansionModule} from "@angular/material/expansion"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {UserRootComponent} from './user-root/user-root.component'; import {routeReuseStrategyProvider} from "./route-strategy.service"; +import {AceEditorComponent} from "./ace-editor/ace-editor.component"; @NgModule({ declarations: [ @@ -49,7 +50,8 @@ import {routeReuseStrategyProvider} from "./route-strategy.service"; LoginComponent, TitleHeaderComponent, RulesComponent, - UserRootComponent + UserRootComponent, + AceEditorComponent ], imports: [ BrowserModule, diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 3ddb696..df3b4d0 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -1,4 +1,4 @@ -import {fakeAsync, tick} from '@angular/core/testing'; +import {fakeAsync, flush, tick} from '@angular/core/testing'; import { FileElement, @@ -694,6 +694,7 @@ describe('FileListComponent', () => { // Assert fixture.detectChanges() + flush(); expect(Page.isCategorySelectedOnFileRow('text.txt', 'TXT')).toBeTruthy(); })) diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 875ab47..4c383b8 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -19,6 +19,7 @@

Setup rules

{{ cat }} + @@ -32,7 +33,8 @@

Setup rules

Script - + +
diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index e348212..3a07feb 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -18,6 +18,7 @@ import {Rule} from "./rule.repository"; import {BreakpointObserver} from "@angular/cdk/layout"; import {RuleService} from "./rule.service"; import {Router} from "@angular/router"; +import {AceEditorComponent} from "../ace-editor/ace-editor.component"; function mockRouterReloadPage() { let router = ngMocks.get(Router); @@ -33,6 +34,7 @@ describe('RulesComponent', () => { .keep(FormsModule) .keep(MatChipsModule) .keep(BreakpointObserver) + .keep(AceEditorComponent) .provide({ provide: Router, useValue: mock() @@ -93,7 +95,7 @@ describe('RulesComponent', () => { fixture.detectChanges(); await page.setRuleName('New rule'); await page.setRuleCategory(['Cat1', 'ChildCat1']); - await page.setRuleScript('return fileName === "child_cat_1.txt"'); + page.setRuleScript('return fileName === "child_cat_1.txt"'); await page.clickOnCreate(); // Assert @@ -250,9 +252,9 @@ class Page { } } - async setRuleScript(script: string) { - let inputHarness = await this.getInputByFloatingLabel('Script'); - await inputHarness.setValue(script); + setRuleScript(script: string) { + let aceEditorComponent = ngMocks.find(AceEditorComponent).componentInstance; + aceEditorComponent.editor?.setValue(script); } async clickOnCreate() { diff --git a/src/app/rules/rules.component.ts b/src/app/rules/rules.component.ts index 8d4d0e1..56afc13 100644 --- a/src/app/rules/rules.component.ts +++ b/src/app/rules/rules.component.ts @@ -5,7 +5,6 @@ import {MatChipInputEvent} from "@angular/material/chips"; import {Rule} from "./rule.repository"; import {Router} from "@angular/router"; - @Component({ selector: 'app-rules', templateUrl: './rules.component.html', @@ -25,6 +24,7 @@ export class RulesComponent { } createOrUpdateRule() { + // TODO: check if all validations are ok? required fields are not empty if (this.ruleToCreateOrUpdate) { if (!this.ruleToCreateOrUpdate.id) { this.ruleService.create(this.ruleToCreateOrUpdate) From 762bdce649d695b023389d9d42cac1782ff3df74 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 1 Feb 2024 16:27:26 +0100 Subject: [PATCH 51/66] [us40] Use ace editor when displaying any script in the list of rules --- src/app/ace-editor/ace-editor.component.html | 2 +- src/app/ace-editor/ace-editor.component.scss | 3 -- src/app/ace-editor/ace-editor.component.ts | 37 ++++++++++++++++---- src/app/rules/rules.component.html | 7 ++-- src/app/rules/rules.component.spec.ts | 6 ++-- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/app/ace-editor/ace-editor.component.html b/src/app/ace-editor/ace-editor.component.html index 421df16..7acf7ec 100644 --- a/src/app/ace-editor/ace-editor.component.html +++ b/src/app/ace-editor/ace-editor.component.html @@ -1 +1 @@ -
+
diff --git a/src/app/ace-editor/ace-editor.component.scss b/src/app/ace-editor/ace-editor.component.scss index 29669cc..8b13789 100644 --- a/src/app/ace-editor/ace-editor.component.scss +++ b/src/app/ace-editor/ace-editor.component.scss @@ -1,4 +1 @@ -.ruleScriptEdit { - height: 200px; -} diff --git a/src/app/ace-editor/ace-editor.component.ts b/src/app/ace-editor/ace-editor.component.ts index ec6e3a4..099bd4f 100644 --- a/src/app/ace-editor/ace-editor.component.ts +++ b/src/app/ace-editor/ace-editor.component.ts @@ -3,6 +3,7 @@ import * as ace from "ace-builds"; import {Ace} from "ace-builds"; import {MatFormFieldControl} from "@angular/material/form-field"; import {Subject} from "rxjs"; +import {BooleanInput, coerceBooleanProperty} from "@angular/cdk/coercion"; import Editor = Ace.Editor; @Component({ @@ -26,7 +27,6 @@ export class AceEditorComponent implements MatFormFieldControl, OnInit { focused: boolean = false; empty: boolean = false; shouldLabelFloat: boolean = true; - disabled: boolean = false; errorState: boolean = false; controlType?: string | undefined = "app-ace-editor"; autofilled?: boolean | undefined; @@ -44,22 +44,45 @@ export class AceEditorComponent implements MatFormFieldControl, OnInit { private _required = false; @Input() - get required() { + get required(): boolean { return this._required; } - set required(req: boolean) { - this._required = req; + set required(req: BooleanInput) { + this._required = coerceBooleanProperty(req); this.stateChanges.next(); } + private _disabled = false; + + @Input() + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value); + this.stateChanges.next(); + } ngOnInit() { if (this.ruleScriptEdit) { - this.editor = ace.edit(this.ruleScriptEdit.nativeElement); - this.editor.session.setMode("ace/mode/javascript"); + this.editor = ace.edit(this.ruleScriptEdit.nativeElement, { + value: this.value, + mode: "ace/mode/javascript", + minLines: 2, + maxLines: 15 + }); + + if (this.disabled) { + this.editor.setReadOnly(true); + this.editor.setHighlightActiveLine(false); + this.editor.setHighlightGutterLine(false); + // hide the cursor + // @ts-ignore + this.editor.renderer.$cursorLayer.element.style.display = "none" + } - this.editor.setValue(this.value); this.editor.on("input", () => { this.valueChange.emit(this.editor?.getValue()); this.stateChanges.next(); diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index 4c383b8..a430bc9 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -34,7 +34,7 @@

Setup rules

Script - +
@@ -54,9 +54,8 @@

Setup rules

*ngIf="!first">  > {{ cat }} -
- {{ rule.script }} -
+ +
  diff --git a/src/app/rules/rules.component.spec.ts b/src/app/rules/rules.component.spec.ts index 3a07feb..0a131ce 100644 --- a/src/app/rules/rules.component.spec.ts +++ b/src/app/rules/rules.component.spec.ts @@ -4,7 +4,7 @@ import {AppModule} from "../app.module"; import {mock, when} from "strong-mock"; import {MatButtonHarness} from "@angular/material/button/testing"; import {HarnessLoader, TestKey} from "@angular/cdk/testing"; -import {ComponentFixture, fakeAsync, tick} from "@angular/core/testing"; +import {ComponentFixture, fakeAsync, flush, tick} from "@angular/core/testing"; import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed"; import {MatInputHarness} from "@angular/material/input/testing"; import {MatFormFieldHarness} from "@angular/material/form-field/testing"; @@ -67,6 +67,7 @@ describe('RulesComponent', () => { // Assert tick(); fixture.detectChanges(); + flush(); expect(Page.getRuleNames()).toEqual(['Electric bill', 'Bank account statement']); expect(Page.getRuleCategory('Electric bill')).toEqual('Electricity  > Bills'); expect(Page.getRuleScript('Electric bill')).toEqual('return fileName === "electricity_bill.pdf"'); @@ -220,8 +221,7 @@ class Page { let rule = ngMocks.findAll("mat-panel-title") .find(row => row.nativeNode.textContent.trim() === name) ?.parent?.parent; - return ngMocks.find(rule, '.ruleScript') - .nativeNode.textContent.trim(); + return ngMocks.find(rule, AceEditorComponent).componentInstance.editor?.getValue() ?? ''; } async clickOnCreateNewRule() { From 839b38b5e90b32704c6d38537697e472dea698dc Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 2 Feb 2024 14:29:27 +0100 Subject: [PATCH 52/66] [us40] Add custom auto-completion to ace editor --- src/app/ace-editor/ace-editor.component.ts | 30 +++++++++++++++++++++- src/app/rules/rules.component.html | 1 - 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/app/ace-editor/ace-editor.component.ts b/src/app/ace-editor/ace-editor.component.ts index 099bd4f..f0d4afa 100644 --- a/src/app/ace-editor/ace-editor.component.ts +++ b/src/app/ace-editor/ace-editor.component.ts @@ -4,7 +4,10 @@ import {Ace} from "ace-builds"; import {MatFormFieldControl} from "@angular/material/form-field"; import {Subject} from "rxjs"; import {BooleanInput, coerceBooleanProperty} from "@angular/cdk/coercion"; +import "ace-builds/src-noconflict/ext-language_tools"; import Editor = Ace.Editor; +import Completer = Ace.Completer; +import Completion = Ace.Completion; @Component({ selector: 'app-ace-editor', @@ -71,9 +74,13 @@ export class AceEditorComponent implements MatFormFieldControl, OnInit { value: this.value, mode: "ace/mode/javascript", minLines: 2, - maxLines: 15 + maxLines: 15, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true }); + this.setupCustomCompletions(); + if (this.disabled) { this.editor.setReadOnly(true); this.editor.setHighlightActiveLine(false); @@ -98,6 +105,27 @@ export class AceEditorComponent implements MatFormFieldControl, OnInit { } } + private setupCustomCompletions() { + let customCompleter: Completer = { + getCompletions(_editor: Ace.Editor, _session: Ace.EditSession, _position: Ace.Point, _prefix: string, callback: Ace.CompleterCallback): void { + let completions: Completion[] = [ + { + value: "fileName", + meta: "local", + score: 100 + }, + { + value: "fileContent", + meta: "local", + score: 100 + } + ]; + callback(null, completions); + } + } + this.editor?.completers.push(customCompleter); + } + setDescribedByIds(ids: string[]): void { } diff --git a/src/app/rules/rules.component.html b/src/app/rules/rules.component.html index a430bc9..eadacec 100644 --- a/src/app/rules/rules.component.html +++ b/src/app/rules/rules.component.html @@ -33,7 +33,6 @@

Setup rules

Script -
From 94977a432b80e0940d9cf5e85263676e4d9c0890 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 2 Feb 2024 15:27:30 +0100 Subject: [PATCH 53/66] [us40] Disable updating the category when it was automatically assigned --- src/app/file-list/file-list.component.html | 3 +- src/app/file-list/file-list.component.spec.ts | 41 +++++++++++++++++++ src/app/file-list/file-list.component.ts | 12 +++++- src/app/rules/rule.service.spec.ts | 29 +++++++++++++ src/app/rules/rule.service.ts | 16 ++++++++ src/app/user-root/user-root.component.ts | 1 + 6 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/app/file-list/file-list.component.html b/src/app/file-list/file-list.component.html index 0e1371c..a6ee4d9 100644 --- a/src/app/file-list/file-list.component.html +++ b/src/app/file-list/file-list.component.html @@ -79,7 +79,8 @@ download Download - diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index df3b4d0..847fc1e 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -37,6 +37,7 @@ import {MatSortModule} from "@angular/material/sort"; import {BreakpointObserver} from "@angular/cdk/layout"; import {FilesCacheService} from "../files-cache/files-cache.service"; import {mockFilesCacheService} from "../files-cache/files-cache.service.spec"; +import {RuleService} from "../rules/rule.service"; function mockRenderAndWaitForChanges() { let fixture = MockRender(FileListComponent, null, {reset: true}); @@ -65,6 +66,10 @@ describe('FileListComponent', () => { provide: FilesCacheService, useValue: mock() }) + .provide({ + provide: RuleService, + useValue: mock() + }) .replace(BrowserAnimationsModule, NoopAnimationsModule) ); @@ -518,6 +523,27 @@ describe('FileListComponent', () => { let result = await page.getCategoriesInDialog(); expect(result).toEqual(['cat1', 'cat1b']) })) + + it('should prevent category assignment when the file categories were automatically assigned', fakeAsync(async () => { + // Arrange + let file = mockFileElement('name'); + mockFilesCacheService([file], true); + + let ruleService = ngMocks.get(RuleService); + let fileToMatchingRuleMap = new Map(); + fileToMatchingRuleMap.set(file.id, "existing rule"); + when(() => ruleService.getFileToMatchingRuleMap()).thenResolve(fileToMatchingRuleMap); + + let fixture = mockRenderAndWaitForChanges(); + let page = new Page(fixture); + + // Act + Page.openItemMenu('name'); + let isMenuDisabled = await page.isMenuAssignCategoryDisabled(); + + // Assert + expect(isMenuDisabled).toBeTruthy(); + })) }) describe('Filter by file name', () => { @@ -859,6 +885,10 @@ class Page { await this.clickMenu('.set-category-file'); } + isMenuAssignCategoryDisabled() { + return this.isMenuDisabled('.set-category-file'); + } + async setCategoryInDialog(category: string) { let testElement = await this.typeCategoryInDialog(category); await testElement.sendKeys(TestKey.ENTER) @@ -939,4 +969,15 @@ class Page { await matMenuHarness?.clickItem({selector: selector}); } + private async isMenuDisabled(selector: string) { + let matMenuHarnesses = await this.loader.getAllHarnesses(MatMenuHarness); + // The menu should be the one opened + let matMenuHarness = await findAsyncSequential(matMenuHarnesses, value => value.isOpen()); + if (!matMenuHarness) { + throw new Error("No menu for selector: " + selector); + } + let menuItems = await matMenuHarness.getItems({selector: selector}); + return menuItems[0].isDisabled(); + } + } diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index 1e9f5a3..3f63287 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -20,6 +20,7 @@ import { } from "@angular/material/autocomplete"; import {MatSort, MatSortable} from "@angular/material/sort"; import {FilesCacheService} from "../files-cache/files-cache.service"; +import {RuleService} from "../rules/rule.service"; export interface FileOrFolderElement { id: string; @@ -65,14 +66,15 @@ export class FileListComponent implements OnInit { isCategoryPanelExpanded = true; private categoryFilters = new Set(); private allFiles: FileOrFolderElement[] = []; + private fileToMatchingRuleMap = new Map(); - constructor(private fileService: FileService, public dialog: MatDialog, private filesCacheService: FilesCacheService) { + constructor(private fileService: FileService, public dialog: MatDialog, private filesCacheService: FilesCacheService, private ruleService: RuleService) { this.fileDataSource.filterPredicate = data => { return this.filterPredicate(data); } } - ngOnInit(): void { + async ngOnInit() { this.baseFolderId = this.filesCacheService.getBaseFolder(); this.allFiles = this.filesCacheService.getAll(); this.populateFilesAndCategories(); @@ -83,6 +85,7 @@ export class FileListComponent implements OnInit { } this.checkForEmptyCategoriesToRemove(); + this.fileToMatchingRuleMap = await this.ruleService.getFileToMatchingRuleMap(); } trashFile(element: FileElement) { @@ -249,6 +252,11 @@ export class FileListComponent implements OnInit { return this.getCategories(fileEl).includes(category); }) } + + + hasMatchingRule(file: FileElement) { + return this.fileToMatchingRuleMap.has(file.id); + } } @Component({ diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index fde9903..7525db7 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -376,6 +376,35 @@ describe('RuleService', () => { // No failure in mock setup })); }) + + describe('getFileToMatchingRuleMap', () => { + it('should return the mapping with one match', async () => { + // Arrange + const service = MockRender(RuleService).point.componentInstance; + + let ruleRepository = ngMocks.findInstance(RuleRepository); + when(() => ruleRepository.findAll()) + .thenResolve([{ + name: 'Matching rule', + category: ['Test'], + script: 'return true;', + fileRuns: [{id: "id-file1", value: true}, {id: "id-file2", value: false}] + }, { + name: 'False rule', + category: ['False'], + script: 'return false;', + fileRuns: [{id: "id-file1", value: false}, {id: "id-file2", value: false}] + }]); + + // Act + let fileToMatchingRuleMap = await service.getFileToMatchingRuleMap(); + + // Assert + expect(fileToMatchingRuleMap) + .toEqual(new Map([['id-file1', 'Matching rule']])); + }) + + }); }); let ruleServiceMock: RuleService; diff --git a/src/app/rules/rule.service.ts b/src/app/rules/rule.service.ts index 13aefe6..207dbc3 100644 --- a/src/app/rules/rule.service.ts +++ b/src/app/rules/rule.service.ts @@ -86,6 +86,22 @@ export class RuleService { return this.ruleRepository.update(rule); } + async getFileToMatchingRuleMap(): Promise> { + let result = new Map(); + let rules = await this.ruleRepository.findAll(); + // Search for rules which have fileRuns evaluated to true + for (let rule of rules) { + if (rule.fileRuns) { + for (const fileRun of rule.fileRuns) { + if (fileRun.value) { + result.set(fileRun.id, rule.name); + } + } + } + } + return result; + } + /** * Run the given rules on the given files and set the associated category for each file that got a matching rule */ diff --git a/src/app/user-root/user-root.component.ts b/src/app/user-root/user-root.component.ts index 449754b..99e34d0 100644 --- a/src/app/user-root/user-root.component.ts +++ b/src/app/user-root/user-root.component.ts @@ -16,6 +16,7 @@ export class UserRootComponent { databaseBackupAndRestoreService.restore() .subscribe(() => { ruleService.runAll().subscribe(); + // TODO: refresh after if there was any update to one of the file categories }); } } From e9dd12aad696b4a260c40764450bfef3d3e9f655 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 8 Feb 2024 12:00:59 +0100 Subject: [PATCH 54/66] [us40] Show tooltip when the category are automatically set by rules --- src/app/app.module.ts | 4 +- src/app/file-list/file-list.component.html | 4 +- src/app/file-list/file-list.component.spec.ts | 46 ++++++++++++------- src/app/file-list/file-list.component.ts | 8 ++++ src/styles.scss | 4 ++ 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7f86c3c..6040fc6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -37,6 +37,7 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; import {UserRootComponent} from './user-root/user-root.component'; import {routeReuseStrategyProvider} from "./route-strategy.service"; import {AceEditorComponent} from "./ace-editor/ace-editor.component"; +import {MatTooltipModule} from "@angular/material/tooltip"; @NgModule({ declarations: [ @@ -77,7 +78,8 @@ import {AceEditorComponent} from "./ace-editor/ace-editor.component"; MatChipsModule, MatSortModule, MatExpansionModule, - MatSnackBarModule + MatSnackBarModule, + MatTooltipModule ], providers: [ httpInterceptorProviders, diff --git a/src/app/file-list/file-list.component.html b/src/app/file-list/file-list.component.html index a6ee4d9..422611e 100644 --- a/src/app/file-list/file-list.component.html +++ b/src/app/file-list/file-list.component.html @@ -59,6 +59,8 @@ Category + calculate + {{cat.name}} @@ -80,7 +82,7 @@ Download diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 847fc1e..718bbbb 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -38,6 +38,8 @@ import {BreakpointObserver} from "@angular/cdk/layout"; import {FilesCacheService} from "../files-cache/files-cache.service"; import {mockFilesCacheService} from "../files-cache/files-cache.service.spec"; import {RuleService} from "../rules/rule.service"; +import {MatTooltipModule} from "@angular/material/tooltip"; +import {MatTooltipHarness} from "@angular/material/tooltip/testing"; function mockRenderAndWaitForChanges() { let fixture = MockRender(FileListComponent, null, {reset: true}); @@ -62,6 +64,7 @@ describe('FileListComponent', () => { .keep(MatChipsModule) .keep(MatSortModule) .keep(BreakpointObserver) + .keep(MatTooltipModule) .provide({ provide: FilesCacheService, useValue: mock() @@ -526,12 +529,13 @@ describe('FileListComponent', () => { it('should prevent category assignment when the file categories were automatically assigned', fakeAsync(async () => { // Arrange - let file = mockFileElement('name'); - mockFilesCacheService([file], true); + let folder = mockFolderElement('Auto'); + let file = mockFileElement('name', folder.id); + mockFilesCacheService([file, folder], true); let ruleService = ngMocks.get(RuleService); let fileToMatchingRuleMap = new Map(); - fileToMatchingRuleMap.set(file.id, "existing rule"); + fileToMatchingRuleMap.set(file.id, "Existing Rule"); when(() => ruleService.getFileToMatchingRuleMap()).thenResolve(fileToMatchingRuleMap); let fixture = mockRenderAndWaitForChanges(); @@ -539,10 +543,15 @@ describe('FileListComponent', () => { // Act Page.openItemMenu('name'); - let isMenuDisabled = await page.isMenuAssignCategoryDisabled(); // Assert + fixture.detectChanges(); + flush(); + let isMenuDisabled = await page.isMenuAssignCategoryDisabled(); expect(isMenuDisabled).toBeTruthy(); + let tooltip = await page.getMenuAssignCategoryTooltip(); + expect(tooltip).toEqual('Automatically assigned by rule "Existing Rule"'); + expect(Page.getTableRows()).toEqual([['name', 'calculateAuto', 'Jan 1, 2000, 12:00:00 AM', '0 B', 'more_vert']]); })) }) @@ -878,15 +887,25 @@ class Page { } async clickMenuTrash() { - await this.clickMenu('.trash-file'); + let matMenuItemHarness = await this.getMenu('.trash-file'); + await matMenuItemHarness.click(); } async clickMenuAssignCategory() { - await this.clickMenu('.set-category-file'); + let matMenuItemHarness = await this.getMenu('.set-category-file'); + await matMenuItemHarness.click(); + } + + async isMenuAssignCategoryDisabled() { + let matMenuItemHarness = await this.getMenu('.set-category-file'); + return matMenuItemHarness.isDisabled(); } - isMenuAssignCategoryDisabled() { - return this.isMenuDisabled('.set-category-file'); + async getMenuAssignCategoryTooltip() { + // There is only one toolTip in our tests, so we can simplify this method + let matTooltipHarness = await this.loader.getHarness(MatTooltipHarness); + await matTooltipHarness.show(); + return matTooltipHarness.getTooltipText(); } async setCategoryInDialog(category: string) { @@ -962,14 +981,7 @@ class Page { return this.fixture.debugElement.parent?.query(By.directive(SelectFileCategoryDialog)).componentInstance as SelectFileCategoryDialog; } - private async clickMenu(selector: string) { - let matMenuHarnesses = await this.loader.getAllHarnesses(MatMenuHarness); - // The menu should be the one opened - let matMenuHarness = await findAsyncSequential(matMenuHarnesses, value => value.isOpen()); - await matMenuHarness?.clickItem({selector: selector}); - } - - private async isMenuDisabled(selector: string) { + private async getMenu(selector: string) { let matMenuHarnesses = await this.loader.getAllHarnesses(MatMenuHarness); // The menu should be the one opened let matMenuHarness = await findAsyncSequential(matMenuHarnesses, value => value.isOpen()); @@ -977,7 +989,7 @@ class Page { throw new Error("No menu for selector: " + selector); } let menuItems = await matMenuHarness.getItems({selector: selector}); - return menuItems[0].isDisabled(); + return menuItems[0]; } } diff --git a/src/app/file-list/file-list.component.ts b/src/app/file-list/file-list.component.ts index 3f63287..3a6b843 100644 --- a/src/app/file-list/file-list.component.ts +++ b/src/app/file-list/file-list.component.ts @@ -257,6 +257,14 @@ export class FileListComponent implements OnInit { hasMatchingRule(file: FileElement) { return this.fileToMatchingRuleMap.has(file.id); } + + getAssignCategoryTooltip(file: FileElement) { + let matchingRule = this.fileToMatchingRuleMap.get(file.id); + if (matchingRule) { + return 'Automatically assigned by rule "' + matchingRule + '"' + } + return ""; + } } @Component({ diff --git a/src/styles.scss b/src/styles.scss index 7e7239a..48c5ee6 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -2,3 +2,7 @@ html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } + +mat-chip-listbox > div { + align-items: center; +} From 5ccacc8e422c3fdcb37292034de8e12e0f8b3712 Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 8 Feb 2024 13:53:35 +0100 Subject: [PATCH 55/66] [us40] Fix missing mock for tests of FileListComponent --- src/app/file-list/file-list.component.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/file-list/file-list.component.spec.ts b/src/app/file-list/file-list.component.spec.ts index 718bbbb..b4cbf85 100644 --- a/src/app/file-list/file-list.component.spec.ts +++ b/src/app/file-list/file-list.component.spec.ts @@ -41,7 +41,11 @@ import {RuleService} from "../rules/rule.service"; import {MatTooltipModule} from "@angular/material/tooltip"; import {MatTooltipHarness} from "@angular/material/tooltip/testing"; -function mockRenderAndWaitForChanges() { +function mockRenderAndWaitForChanges(mockRuleService: boolean = true) { + if (mockRuleService) { + let ruleService = ngMocks.get(RuleService); + when(() => ruleService.getFileToMatchingRuleMap()).thenResolve(new Map()); + } let fixture = MockRender(FileListComponent, null, {reset: true}); try { tick(); @@ -538,7 +542,7 @@ describe('FileListComponent', () => { fileToMatchingRuleMap.set(file.id, "Existing Rule"); when(() => ruleService.getFileToMatchingRuleMap()).thenResolve(fileToMatchingRuleMap); - let fixture = mockRenderAndWaitForChanges(); + let fixture = mockRenderAndWaitForChanges(false); let page = new Page(fixture); // Act @@ -551,7 +555,7 @@ describe('FileListComponent', () => { expect(isMenuDisabled).toBeTruthy(); let tooltip = await page.getMenuAssignCategoryTooltip(); expect(tooltip).toEqual('Automatically assigned by rule "Existing Rule"'); - expect(Page.getTableRows()).toEqual([['name', 'calculateAuto', 'Jan 1, 2000, 12:00:00 AM', '0 B', 'more_vert']]); + expect(Page.getTableRows()).toEqual([['name', 'calculate Auto', 'Jan 1, 2000, 12:00:00 AM', '0 B', 'more_vert']]); })) }) From 7cfdf865d787802ff8abdce221d79a5ad4b0650a Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 8 Feb 2024 17:48:09 +0100 Subject: [PATCH 56/66] [us40] Compress database before uploading + Extract on download --- karma.conf.js | 2 +- package-lock.json | 75 +++++++++++++++--- package.json | 1 + ...atabase-backup-and-restore.service.spec.ts | 67 ++++++++-------- .../database-backup-and-restore.service.ts | 34 ++++++-- src/app/rules/rule.service.spec.ts | 2 +- testing-assets/database/db.backup.zip | Bin 0 -> 377 bytes {src/app => testing-assets}/rules/dummy.pdf | Bin 8 files changed, 125 insertions(+), 56 deletions(-) create mode 100644 testing-assets/database/db.backup.zip rename {src/app => testing-assets}/rules/dummy.pdf (100%) diff --git a/karma.conf.js b/karma.conf.js index 954ff7c..732775e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -38,7 +38,7 @@ module.exports = function (config) { restartOnFileChange: true, files: [ { - pattern: 'src/app/rules/dummy.pdf', + pattern: 'testing-assets/**', included: false, watched: false, served: true diff --git a/package-lock.json b/package-lock.json index 0fefe8e..c45f70d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "dexie": "^3.2.4", "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", + "jszip": "^3.10.1", "ngx-filesize": "^3.0.2", "pdfjs-dist": "^3.0.0", "rxjs": "~7.8.0", @@ -5892,8 +5893,7 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -7924,6 +7924,11 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immutable": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", @@ -7986,8 +7991,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "devOptional": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "4.1.1", @@ -8255,8 +8259,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isbinaryfile": { "version": "4.0.10", @@ -8722,6 +8725,44 @@ "node >= 0.2.0" ] }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/karma": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", @@ -9185,6 +9226,14 @@ } } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10616,8 +10665,7 @@ "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -11153,8 +11201,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/progress": { "version": "2.0.3", @@ -12084,6 +12131,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -13404,8 +13456,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", diff --git a/package.json b/package.json index 667b047..7109e42 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dexie": "^3.2.4", "dexie-export-import": "^4.0.7", "filesize": "^9.0.11", + "jszip": "^3.10.1", "ngx-filesize": "^3.0.2", "pdfjs-dist": "^3.0.0", "rxjs": "~7.8.0", diff --git a/src/app/database/database-backup-and-restore.service.spec.ts b/src/app/database/database-backup-and-restore.service.spec.ts index f6ccc01..16afaf4 100644 --- a/src/app/database/database-backup-and-restore.service.spec.ts +++ b/src/app/database/database-backup-and-restore.service.spec.ts @@ -20,40 +20,27 @@ import {FilesCacheService} from "../files-cache/files-cache.service"; import {FileElement} from "../file-list/file-list.component"; import {Rule} from "../rules/rule.repository"; import {mockFileService} from "../file-list/file.service.spec"; +import JSZip from "jszip"; -function setupMockForRestore(dbBackupFile: FileElement) { +async function setupMockForRestore(dbBackupFileElement: FileElement) { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Automatic restore", 2, "Downloading last backup")) + when(() => backgroundTaskService.showProgress("Automatic restore", 3, "Downloading last backup")) .thenReturn(progress); - when(() => progress.next({index: 2, value: 0, description: "Importing last backup"})).thenReturn(); - when(() => progress.next({index: 2, value: 100})).thenReturn(); + when(() => progress.next({index: 2, value: 0, description: "Extracting last backup"})).thenReturn(); + when(() => progress.next({index: 3, value: 0, description: "Importing last backup"})).thenReturn(); + when(() => progress.next({index: 3, value: 100})).thenReturn(); let fileService = mockFileService(); - when(() => fileService.downloadFile(dbBackupFile, progress)) - .thenReturn(mustBeConsumedAsyncObservable(new Blob([JSON.stringify({ - "formatName": "dexie", - "formatVersion": 1, - "data": { - "databaseName": "StoreMyDocsDB", - "databaseVersion": 3, - "tables": [{"name": "rules", "schema": "++id", "rowCount": 1}], - "data": [{ - "tableName": "rules", - "inbound": true, - "rows": [{ - "name": "TestRule", - "category": ["Test1", "ChildTest1"], - "script": "return true", - "id": 1, - "$types": {"category": "arrayNonindexKeys"} - }] - }] - } - })]))); + let compressedDbBackupFile = await fetch("/base/testing-assets/database/db.backup.zip"); + let compressedDbBackupBlob = await compressedDbBackupFile.blob(); + when(() => fileService.downloadFile(dbBackupFileElement, progress)) + .thenReturn(mustBeConsumedAsyncObservable(compressedDbBackupBlob)); } +const EMPTY_DB_BACKUP = `{"formatName":"dexie","formatVersion":1,"data":{"databaseName":"StoreMyDocsDB","databaseVersion":3,"tables":[{"name":"rules","schema":"++id","rowCount":0}],"data":[{"tableName":"rules","inbound":true,"rows":[]}]}}`; + describe('DatabaseBackupAndRestoreService', () => { beforeEach(() => MockBuilder(DatabaseBackupAndRestoreService, AppModule) .provide({ @@ -82,13 +69,12 @@ describe('DatabaseBackupAndRestoreService', () => { describe('restore', () => { it('The database should be restored', fakeAsync(async () => { // Arrange - let localStorageMock = getLocalStorageMock(); when(() => localStorageMock.getItem('last_db_backup_time')).thenReturn(null); when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); let dbBackupFile = mockFileElement('db.backup'); - setupMockForRestore(dbBackupFile); + await setupMockForRestore(dbBackupFile); let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -98,7 +84,6 @@ describe('DatabaseBackupAndRestoreService', () => { let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore()); // Assert - tick(); // We need to explicitly wait for the restore to finish await restorePromise; @@ -119,7 +104,7 @@ describe('DatabaseBackupAndRestoreService', () => { when(() => localStorageMock.setItem('last_db_backup_time', It.isAny())).thenReturn(); let dbBackupFile = mockFileElement('db.backup'); - setupMockForRestore(dbBackupFile); + await setupMockForRestore(dbBackupFile); let databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -136,8 +121,6 @@ describe('DatabaseBackupAndRestoreService', () => { let restorePromise = lastValueFrom(databaseBackupAndRestoreService.restore()); // Assert - tick(); - // We need to explicitly wait for the restore to finish await restorePromise; @@ -184,9 +167,10 @@ describe('DatabaseBackupAndRestoreService', () => { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Backup", 2, "Creating backup")) + when(() => backgroundTaskService.showProgress("Backup", 3, "Creating backup")) .thenReturn(progress); - when(() => progress.next({index: 2, description: "Uploading backup", value: 0})).thenReturn(); + when(() => progress.next({index: 2, description: "Compressing backup", value: 0})).thenReturn(); + when(() => progress.next({index: 3, description: "Uploading backup", value: 0})).thenReturn(); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; @@ -194,7 +178,8 @@ describe('DatabaseBackupAndRestoreService', () => { mockFilesCacheService([]); let fileUploadService = ngMocks.get(FileUploadService); - when(() => fileUploadService.upload(It.isObject({blob: It.isAny(), name: "db.backup"}))) + let blobUploadCapture = It.willCapture(); + when(() => fileUploadService.upload(It.isObject({blob: blobUploadCapture, name: "db.backup"}))) .thenReturn(mustBeConsumedAsyncObservable({ type: HttpEventType.Response } as HttpResponse)); @@ -205,6 +190,15 @@ describe('DatabaseBackupAndRestoreService', () => { // Assert // No failure in mock setup await backupPromise; + + // The blob should be a compressed zip, we have to unzip it and check its content + let blob = await blobUploadCapture.value; + let stringResult: string | undefined; + if (blob) { + let zipResult = await JSZip.loadAsync(blob); + stringResult = await zipResult.file("db.backup")?.async("string"); + } + expect(stringResult).toEqual(EMPTY_DB_BACKUP) }) it('should overwrite the existing backup file when there is already an existing backup', async () => { @@ -214,9 +208,10 @@ describe('DatabaseBackupAndRestoreService', () => { let backgroundTaskService = mockBackgroundTaskService(); let progress = mock>(); - when(() => backgroundTaskService.showProgress("Backup", 2, "Creating backup")) + when(() => backgroundTaskService.showProgress("Backup", 3, "Creating backup")) .thenReturn(progress); - when(() => progress.next({index: 2, description: "Uploading backup", value: 0})).thenReturn(); + when(() => progress.next({index: 2, description: "Compressing backup", value: 0})).thenReturn(); + when(() => progress.next({index: 3, description: "Uploading backup", value: 0})).thenReturn(); when(() => backgroundTaskService.updateProgress(progress, It.isAny())).thenReturn(); const databaseBackupAndRestoreService = MockRender(DatabaseBackupAndRestoreService).point.componentInstance; diff --git a/src/app/database/database-backup-and-restore.service.ts b/src/app/database/database-backup-and-restore.service.ts index aa2624c..a2bec5e 100644 --- a/src/app/database/database-backup-and-restore.service.ts +++ b/src/app/database/database-backup-and-restore.service.ts @@ -7,6 +7,8 @@ import {FileService} from "../file-list/file.service"; import {FileElement, isFileElement} from "../file-list/file-list.component"; import {BackgroundTaskService} from "../background-task/background-task.service"; import {FilesCacheService} from "../files-cache/files-cache.service"; +import JSZip from "jszip"; +import {fromPromise} from "rxjs/internal/observable/innerFrom"; @Injectable() export class DatabaseBackupAndRestoreService { @@ -22,9 +24,19 @@ export class DatabaseBackupAndRestoreService { } backup() { - let progress = this.backgroundTaskService.showProgress('Backup', 2, "Creating backup"); + let progress = this.backgroundTaskService.showProgress('Backup', 3, "Creating backup"); return from(exportDB(db)) - .pipe(tap(() => progress.next({index: 2, value: 0, description: "Uploading backup"})), + .pipe( + tap(() => progress.next({index: 2, value: 0, description: "Compressing backup"})), + mergeMap(blob => { + const zip = new JSZip(); + zip.file("db.backup", blob); + return fromPromise(zip.generateAsync({ + type: "blob", + compression: "DEFLATE" + })); + }), + tap(() => progress.next({index: 3, value: 0, description: "Uploading backup"})), mergeMap(blob => { let dbFile = this.findExistingDbFile(); return this.fileUploadService.upload({name: DatabaseBackupAndRestoreService.DB_NAME, blob}, dbFile?.id); @@ -49,14 +61,24 @@ export class DatabaseBackupAndRestoreService { let lastDbBackupTime = this.getLastDbBackupTime(); let modifiedTime = dbFile?.modifiedTime ?? Date.now(); if (dbFile && modifiedTime > lastDbBackupTime) { - let progress = this.backgroundTaskService.showProgress('Automatic restore', 2, "Downloading last backup"); + let progress = this.backgroundTaskService.showProgress('Automatic restore', 3, "Downloading last backup"); return this.fileService.downloadFile(dbFile, progress) .pipe( - tap(() => progress.next({index: 2, value: 0, description: 'Importing last backup'})), + tap(() => progress.next({index: 2, value: 0, description: 'Extracting last backup'})), mergeMap(dbDownloadResponse => { - return from(db.import(dbDownloadResponse, {clearTablesBeforeImport: true})); + let zip = new JSZip(); + return fromPromise(zip.loadAsync(dbDownloadResponse).then(value => { + return value.file("db.backup")?.async("blob"); + })); + }), + tap(() => progress.next({index: 3, value: 0, description: 'Importing last backup'})), + mergeMap(dbBlob => { + if (!dbBlob) { + return of(); + } + return from(db.import(dbBlob, {clearTablesBeforeImport: true})); }), - tap(() => progress.next({index: 2, value: 100})), + tap(() => progress.next({index: 3, value: 100})), map(() => void 0), finalize(() => this.updateLastDbBackupTime()) ); diff --git a/src/app/rules/rule.service.spec.ts b/src/app/rules/rule.service.spec.ts index 7525db7..daa929b 100644 --- a/src/app/rules/rule.service.spec.ts +++ b/src/app/rules/rule.service.spec.ts @@ -337,7 +337,7 @@ describe('RuleService', () => { let file = mockFileElement('dummy.pdf'); file.mimeType = 'application/pdf'; - let dummyPdfResponse = await fetch('/base/src/app/rules/dummy.pdf'); + let dummyPdfResponse = await fetch('/base/testing-assets/rules/dummy.pdf'); let dummyPdfBlob = await dummyPdfResponse.blob(); when(() => fileService.downloadFile(file, progress)) .thenReturn(mustBeConsumedAsyncObservable(dummyPdfBlob)); diff --git a/testing-assets/database/db.backup.zip b/testing-assets/database/db.backup.zip new file mode 100644 index 0000000000000000000000000000000000000000..a8c333006761ed621a74c424db07c3ddc478dc8e GIT binary patch literal 377 zcmWIWW@Zs#U|`^2cv9yP5vTgd^CFNJ%*epN$sog!lBAcEn4Dc&5E{bC!0hw-NNO|? zmsW5yFtWU0W?%plp%eD5J7mDqT0V~{vRs)v=`=@92VX&{_!UJHHl>S8ieh3Ht$N=l zT*q=s;OC9|mVRs&tXaI_%Umxce0(NwCHRkXmtMt%V%BRL7;UHg&QP*zv}He(8`ZA1 zZu)|IZ_RfFD4Y^iesP%LpJU*BR@RL>6STJ(%(m1vZT%nm!q)py#C{`IZN97Kum1$P z>1HgGqRmOM0JTwp=|L`*!h)&vH+!&OJL}`!jRf8NqEff7^qn zf0wtLb3kBb2 Date: Fri, 9 Feb 2024 14:21:59 +0100 Subject: [PATCH 57/66] [us40] Support showing multiple background task progress at the same time --- angular.json | 8 +- .../background-task.service.spec.ts | 51 +++++++++++- .../background-task.service.ts | 82 +++++++++++++------ .../progress-indicator.snack-bar.html | 13 +-- 4 files changed, 121 insertions(+), 33 deletions(-) diff --git a/angular.json b/angular.json index 3922ba5..a1f02d9 100644 --- a/angular.json +++ b/angular.json @@ -44,7 +44,13 @@ "src/styles.scss" ], "scripts": [], - "webWorkerTsConfig": "tsconfig.worker.json" + "webWorkerTsConfig": "tsconfig.worker.json", + "allowedCommonJsDependencies": [ + "filesize", + "ace-builds", + "jszip", + "pdfjs-dist" + ] }, "configurations": { "production": { diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 2ba6949..250b997 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -32,6 +32,7 @@ describe('BackgroundTaskService', () => { backgroundTaskService.showProgress("Test", 2, "Doing first test"); // Assert + fixture.detectChanges(); let result = await Page.getProgressMessage(); expect(result).toEqual("1/2 0% Test: Doing first test..."); }) @@ -74,7 +75,7 @@ describe('BackgroundTaskService', () => { let resultMessage = await Page.getProgressMessage(); expect(resultMessage).toEqual("2/2 100% Test finished!"); tick(3000); - // The message should be gone after 5 seconds at least + // The message should be gone after 3 seconds resultMessage = await Page.getProgressMessage(); expect(resultMessage).toEqual(undefined); })) @@ -98,6 +99,54 @@ describe('BackgroundTaskService', () => { let resultMessage = await Page.getProgressMessage(); expect(resultMessage).toEqual(undefined); })) + + it('Should support showing a second ongoing task', async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + let progress1 = backgroundTaskService.showProgress("Test", 4, "Doing first test"); + fixture.detectChanges(); + + // Act + let progress2 = backgroundTaskService.showProgress("Second test", 2, "Starting second test"); + progress1.next({ + index: 2, + value: 50, + description: "Doing more test" + }) + progress2.next({ + index: 1, + value: 50, + description: "Doing second test" + }) + + // Assert + fixture.detectChanges(); + let result = await Page.getProgressMessage(); + expect(result).toEqual("2/4 37% Test: Doing more test...1/2 25% Second test: Doing second test..."); + }) + + it('Should support hiding the first ongoing task and still showing the second ongoing task', fakeAsync(async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + let progress1 = backgroundTaskService.showProgress("Test", 4, "Doing first test"); + fixture.detectChanges(); + + // Act + backgroundTaskService.showProgress("Second test", 2, "Starting second test"); + progress1.next({ + index: 4, + value: 100 + }) + + // Assert + fixture.detectChanges(); + tick(3000); + fixture.detectChanges(); + let result = await Page.getProgressMessage(); + expect(result).toEqual("1/2 0% Second test: Starting second test..."); + })) }) describe('updateProgress', () => { it('Should update progress with intermediate download progress event', () => { diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index 1edc3b8..3e920d0 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -1,7 +1,7 @@ -import {Component, Inject, Injectable} from '@angular/core'; +import {Component, Injectable} from '@angular/core'; import {BehaviorSubject} from "rxjs"; -import {MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarModule, MatSnackBarRef} from "@angular/material/snack-bar"; -import {NgIf} from "@angular/common"; +import {MatSnackBar, MatSnackBarModule, MatSnackBarRef} from "@angular/material/snack-bar"; +import {NgForOf, NgIf} from "@angular/common"; import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; @Injectable({ @@ -9,6 +9,8 @@ import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/ht }) export class BackgroundTaskService { + private snackBarRef?: MatSnackBarRef; + constructor(private snackBar: MatSnackBar) { } @@ -24,7 +26,12 @@ export class BackgroundTaskService { stepAmount: stepAmount, progress: progress }; - this.openSnackBar(progressData); + + this.showSnackBar(); + + if (this.snackBarRef) { + this.snackBarRef.instance.addProgressData(progressData) + } return progress; } @@ -46,8 +53,10 @@ export class BackgroundTaskService { } } - private openSnackBar(data: ProgressData) { - return this.snackBar.openFromComponent(SnackBarProgressIndicatorComponent, {data: data}); + private showSnackBar() { + if (!this.snackBarRef) { + this.snackBarRef = this.snackBar.openFromComponent(SnackBarProgressIndicatorComponent); + } } } @@ -76,36 +85,57 @@ export interface Progress { standalone: true, imports: [ MatSnackBarModule, - NgIf + NgIf, + NgForOf ] }) class SnackBarProgressIndicatorComponent { - progress: Progress; - - constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ProgressData, snackBarRef: MatSnackBarRef) { - this.progress = data.progress.getValue(); - - data.progress.subscribe(progress => { - let noStepYet = !this.progress.description; - this.progress = progress; - if (this.isFinished()) { - if (noStepYet) { - // There was no step, and it's already finished, - // we can simply dismiss the message since there is actually nothing to inform the users about - snackBarRef.dismiss(); + public dataList: ProgressData[] = []; + private isEmpty = true; + + constructor(private snackBarRef: MatSnackBarRef) { + } + + getTotalProgress(data: ProgressData) { + return Math.floor(((data.progress.value.index - 1) * 100 + data.progress.value.value) / data.stepAmount); + } + + isFinished(data: ProgressData) { + return data.progress.value.index === data.stepAmount && data.progress.value.value === 100; + } + + public addProgressData(progressData: ProgressData) { + let initialProgress = progressData.progress.value; + this.dataList.push(progressData); + if (initialProgress.description) { + this.isEmpty = false; + } + progressData.progress.subscribe(progress => { + if (this.isFinished(progressData)) { + if (this.isEmpty) { + this.removeProgressData(progressData); } else { - // The user must see that something happened - snackBarRef._dismissAfter(3000); + setTimeout(() => { + this.removeProgressData(progressData); + }, 3000); } + } else if (progress.description) { + this.isEmpty = false; } }) } - getTotalProgress() { - return Math.floor(((this.progress.index - 1) * 100 + this.progress.value) / this.data.stepAmount); + private dismissIfEmpty() { + if (this.dataList.length === 0) { + this.snackBarRef.dismiss(); + } } - isFinished() { - return this.progress.index === this.data.stepAmount && this.progress.value === 100; + private removeProgressData(progressData: ProgressData) { + let index = this.dataList.indexOf(progressData); + if (index > -1) { + this.dataList.splice(index, 1) + } + this.dismissIfEmpty(); } } diff --git a/src/app/background-task/progress-indicator.snack-bar.html b/src/app/background-task/progress-indicator.snack-bar.html index 0161887..257dc3f 100644 --- a/src/app/background-task/progress-indicator.snack-bar.html +++ b/src/app/background-task/progress-indicator.snack-bar.html @@ -1,5 +1,8 @@ - - {{ progress.index }}/{{ data.stepAmount }} {{ getTotalProgress() }}% {{ data.globalDescription }} - : {{ progress.description }}... - finished! - +
+
+ {{ data.progress.value.index }}/{{ data.stepAmount }} {{ getTotalProgress(data) }} + % {{ data.globalDescription }} + : {{ data.progress.value.description }}... + finished! +
+
From 12994d02839066ca23f3255d53977c67c1b11921 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 9 Feb 2024 14:36:43 +0100 Subject: [PATCH 58/66] [us40] Fix taskbar not showing anymore after first task finished --- .../background-task.service.spec.ts | 25 ++++++++++++++++++- .../background-task.service.ts | 7 +++++- .../progress-indicator.snack-bar.html | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 250b997..5411858 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -3,7 +3,7 @@ import {mock} from "strong-mock"; import {MockBuilder, MockInstance, MockRender} from "ng-mocks"; import {AppModule} from "../app.module"; import {MatSnackBarModule} from "@angular/material/snack-bar"; -import {fakeAsync, tick} from "@angular/core/testing"; +import {fakeAsync, flush, tick} from "@angular/core/testing"; import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; import {BehaviorSubject} from "rxjs"; import {HttpDownloadProgressEvent, HttpEventType, HttpResponse, HttpUploadProgressEvent} from "@angular/common/http"; @@ -147,6 +147,29 @@ describe('BackgroundTaskService', () => { let result = await Page.getProgressMessage(); expect(result).toEqual("1/2 0% Second test: Starting second test..."); })) + + it('Should start showing a second task progress after the first task finished', fakeAsync(async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + // Task with no actual step to show which is dismissed immediately + let progress1 = backgroundTaskService.showProgress("Test", 4); + progress1.next({ + index: 4, + value: 100 + }); + tick(); + + // Act + backgroundTaskService.showProgress("Second test", 2, "Starting second test"); + + // Assert + tick(); + fixture.detectChanges(); + let result = await Page.getProgressMessage(); + expect(result).toEqual("1/2 0% Second test: Starting second test..."); + flush(); + })) }) describe('updateProgress', () => { it('Should update progress with intermediate download progress event', () => { diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index 3e920d0..a95a69f 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -56,6 +56,10 @@ export class BackgroundTaskService { private showSnackBar() { if (!this.snackBarRef) { this.snackBarRef = this.snackBar.openFromComponent(SnackBarProgressIndicatorComponent); + this.snackBarRef.afterDismissed() + .subscribe(() => { + this.snackBarRef = undefined; + }); } } } @@ -97,7 +101,8 @@ class SnackBarProgressIndicatorComponent { } getTotalProgress(data: ProgressData) { - return Math.floor(((data.progress.value.index - 1) * 100 + data.progress.value.value) / data.stepAmount); + let totalProgress = Math.floor(((data.progress.value.index - 1) * 100 + data.progress.value.value) / data.stepAmount); + return totalProgress + '%'; } isFinished(data: ProgressData) { diff --git a/src/app/background-task/progress-indicator.snack-bar.html b/src/app/background-task/progress-indicator.snack-bar.html index 257dc3f..d03cde8 100644 --- a/src/app/background-task/progress-indicator.snack-bar.html +++ b/src/app/background-task/progress-indicator.snack-bar.html @@ -1,7 +1,7 @@
{{ data.progress.value.index }}/{{ data.stepAmount }} {{ getTotalProgress(data) }} - % {{ data.globalDescription }} + {{ data.globalDescription }} : {{ data.progress.value.description }}... finished!
From 8d2e6c0623a3b0c99df24ac46bc34582b02372f8 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 9 Feb 2024 15:31:10 +0100 Subject: [PATCH 59/66] [us40] Warn the user before closing the application when there are tasks in progress --- .../background-task.service.spec.ts | 36 +++++++++++++++++++ .../background-task.service.ts | 12 +++++++ src/app/user-root/user-root.component.ts | 12 +++++-- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 5411858..4c1e1e6 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -247,6 +247,42 @@ describe('BackgroundTaskService', () => { description: "Testing download" }); }); + + }) + + describe('isEmpty', () => { + it('Should return false when there is a task in progress', async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + backgroundTaskService.showProgress("Test", 2, "Doing first test"); + + // Act + let result = backgroundTaskService.isEmpty(); + + // Assert + expect(result).toBeFalsy(); + }); + + it('Should return true when there is no more task in progress (even if there is still a message)', async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + let progress = backgroundTaskService.showProgress("Test", 2, "Doing first test"); + // Finish the task, a message should be shown for 3 seconds + progress.next({ + index: 2, + value: 100 + }); + fixture.detectChanges(); + + // Act + let result = backgroundTaskService.isEmpty(); + + // Assert + fixture.detectChanges(); + expect(result).toBeTruthy(); + }); }) }); diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index a95a69f..c9ec3d1 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -53,6 +53,14 @@ export class BackgroundTaskService { } } + isEmpty() { + if (this.snackBarRef) { + // A message is shown, but we must check if the task are already finished + return this.snackBarRef.instance.isAllTaskFinished(); + } + return true; + } + private showSnackBar() { if (!this.snackBarRef) { this.snackBarRef = this.snackBar.openFromComponent(SnackBarProgressIndicatorComponent); @@ -130,6 +138,10 @@ class SnackBarProgressIndicatorComponent { }) } + isAllTaskFinished() { + return this.dataList.every(this.isFinished); + } + private dismissIfEmpty() { if (this.dataList.length === 0) { this.snackBarRef.dismiss(); diff --git a/src/app/user-root/user-root.component.ts b/src/app/user-root/user-root.component.ts index 99e34d0..5b55f61 100644 --- a/src/app/user-root/user-root.component.ts +++ b/src/app/user-root/user-root.component.ts @@ -1,9 +1,10 @@ -import {Component} from '@angular/core'; +import {Component, HostListener} from '@angular/core'; import {DatabaseBackupAndRestoreService} from "../database/database-backup-and-restore.service"; import {RuleRepository} from "../rules/rule.repository"; import {FileUploadService} from "../file-upload/file-upload.service"; import {FilesCacheService} from "../files-cache/files-cache.service"; import {RuleService} from "../rules/rule.service"; +import {BackgroundTaskService} from "../background-task/background-task.service"; @Component({ selector: 'app-user-root', @@ -12,11 +13,18 @@ import {RuleService} from "../rules/rule.service"; providers: [RuleRepository, RuleService, DatabaseBackupAndRestoreService, FileUploadService, FilesCacheService] }) export class UserRootComponent { - constructor(databaseBackupAndRestoreService: DatabaseBackupAndRestoreService, ruleService: RuleService) { + constructor(databaseBackupAndRestoreService: DatabaseBackupAndRestoreService, ruleService: RuleService, + private backgroundTaskService: BackgroundTaskService) { databaseBackupAndRestoreService.restore() .subscribe(() => { ruleService.runAll().subscribe(); // TODO: refresh after if there was any update to one of the file categories }); } + + @HostListener('window:beforeunload') + beforeUnload(): boolean { + // Warn the user about running tasks before leaving the page + return this.backgroundTaskService.isEmpty(); + } } From 4e7dd671152454af306de48af2810de6199a8383 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 9 Feb 2024 15:42:47 +0100 Subject: [PATCH 60/66] [us40] Fix showing messages for empty tasks when another one is not finished --- .../background-task.service.spec.ts | 22 +++++++++++++++++++ .../background-task.service.ts | 10 ++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 4c1e1e6..0e9f5d6 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -170,6 +170,28 @@ describe('BackgroundTaskService', () => { expect(result).toEqual("1/2 0% Second test: Starting second test..."); flush(); })) + + it('Should not show message for a second task finishing before the first and with no actual step', fakeAsync(async () => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + backgroundTaskService.showProgress("Test", 4, "Doing first test"); + fixture.detectChanges(); + let progress2 = backgroundTaskService.showProgress("Second test", 2); + + // Act + // Finish the second task with no actual step + progress2.next({ + index: 2, + value: 100 + }) + + // Assert + fixture.detectChanges(); + let result = await Page.getProgressMessage(); + expect(result).toEqual("1/4 0% Test: Doing first test..."); + flush(); + })) }) describe('updateProgress', () => { it('Should update progress with intermediate download progress event', () => { diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index c9ec3d1..1764b14 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -103,7 +103,6 @@ export interface Progress { }) class SnackBarProgressIndicatorComponent { public dataList: ProgressData[] = []; - private isEmpty = true; constructor(private snackBarRef: MatSnackBarRef) { } @@ -120,12 +119,11 @@ class SnackBarProgressIndicatorComponent { public addProgressData(progressData: ProgressData) { let initialProgress = progressData.progress.value; this.dataList.push(progressData); - if (initialProgress.description) { - this.isEmpty = false; - } + let isEmpty = !initialProgress.description; + progressData.progress.subscribe(progress => { if (this.isFinished(progressData)) { - if (this.isEmpty) { + if (isEmpty) { this.removeProgressData(progressData); } else { setTimeout(() => { @@ -133,7 +131,7 @@ class SnackBarProgressIndicatorComponent { }, 3000); } } else if (progress.description) { - this.isEmpty = false; + isEmpty = false; } }) } From 4a54fb070aaa2a5a44bb32cb444782554a3af8db Mon Sep 17 00:00:00 2001 From: Musholic Date: Thu, 15 Feb 2024 15:50:49 +0100 Subject: [PATCH 61/66] [us40] Support for scheduling background tasks by preventing concurrent run of the same task --- .../background-task.service.spec.ts | 115 +++++++++++++++++- .../background-task.service.ts | 30 ++++- 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/app/background-task/background-task.service.spec.ts b/src/app/background-task/background-task.service.spec.ts index 0e9f5d6..129abe8 100644 --- a/src/app/background-task/background-task.service.spec.ts +++ b/src/app/background-task/background-task.service.spec.ts @@ -5,8 +5,9 @@ import {AppModule} from "../app.module"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {fakeAsync, flush, tick} from "@angular/core/testing"; import {BrowserAnimationsModule, NoopAnimationsModule} from "@angular/platform-browser/animations"; -import {BehaviorSubject} from "rxjs"; +import {BehaviorSubject, delay, map, of} from "rxjs"; import {HttpDownloadProgressEvent, HttpEventType, HttpResponse, HttpUploadProgressEvent} from "@angular/common/http"; +import {mustBeConsumedAsyncObservable} from "../../testing/common-testing-function.spec"; describe('BackgroundTaskService', () => { beforeEach(() => MockBuilder(BackgroundTaskService, AppModule) @@ -306,6 +307,118 @@ describe('BackgroundTaskService', () => { expect(result).toBeTruthy(); }); }) + describe('schedule', () => { + it('prevent duplicates of the same task when they are not run yet', fakeAsync(() => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + + // Act + let result1 = 0; + let result2 = 0; + let task1 = () => { + result1++; + return mustBeConsumedAsyncObservable(undefined); + }; + // First instance will be running now + backgroundTaskService.schedule("task1", task1).subscribe(); + // Second instance will be scheduled for later + backgroundTaskService.schedule("task1", task1).subscribe(); + // Third instance will be dropped since it's already scheduled for later + backgroundTaskService.schedule("task1", task1).subscribe(); + + backgroundTaskService.schedule("task2", () => { + result2++; + return mustBeConsumedAsyncObservable(undefined); + }).subscribe(); + + // Assert + tick(); + expect(result1).toEqual(2); + expect(result2).toEqual(1); + })); + + it('allows duplicates of the same task if the first task already completed', fakeAsync(() => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + // Schedule a first instance of task1 + let result1 = 0; + let task1 = () => { + result1++; + return mustBeConsumedAsyncObservable(undefined); + }; + backgroundTaskService.schedule("task1", task1).subscribe(); + tick(); + + // Act + backgroundTaskService.schedule("task1", task1).subscribe(); + + // Assert + tick(); + expect(result1).toEqual(2); + })); + + it('allows duplicates of the same task if the first task already completed two times', fakeAsync(() => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + // Schedule a first instance of task1 + let result1a = 0; + let result1b = 0; + let result1c = 0; + backgroundTaskService.schedule("task1", () => { + result1a++; + return mustBeConsumedAsyncObservable(undefined); + }).subscribe(); + tick(); + backgroundTaskService.schedule("task1", () => { + result1b++; + return mustBeConsumedAsyncObservable(undefined); + }).subscribe(); + tick(); + + // Act + backgroundTaskService.schedule("task1", () => { + result1c++; + return mustBeConsumedAsyncObservable(undefined); + }).subscribe(); + + // Assert + tick(); + expect(result1a).toEqual(1); + expect(result1b).toEqual(1); + expect(result1c).toEqual(1); + })); + + it('delay duplicate task if the first task is currently running', fakeAsync(() => { + // Arrange + let fixture = MockRender(BackgroundTaskService); + const backgroundTaskService = fixture.point.componentInstance; + // Schedule a first instance of task1 with a 5 seconds delay + let result1 = 0; + backgroundTaskService.schedule("task1", () => { + return of(undefined).pipe( + delay(5000), + map(() => { + result1++; + })); + }).subscribe(); + tick(); + + // Act + backgroundTaskService.schedule("task1", () => { + result1++; + return mustBeConsumedAsyncObservable(undefined); + }).subscribe(); + + // Assert + tick(); + expect(result1).toEqual(0); + tick(5000); + expect(result1).toEqual(2); + })); + }) }); class Page { diff --git a/src/app/background-task/background-task.service.ts b/src/app/background-task/background-task.service.ts index 1764b14..2a0e6b3 100644 --- a/src/app/background-task/background-task.service.ts +++ b/src/app/background-task/background-task.service.ts @@ -1,5 +1,5 @@ import {Component, Injectable} from '@angular/core'; -import {BehaviorSubject} from "rxjs"; +import {BehaviorSubject, mergeMap, Observable, of, share} from "rxjs"; import {MatSnackBar, MatSnackBarModule, MatSnackBarRef} from "@angular/material/snack-bar"; import {NgForOf, NgIf} from "@angular/common"; import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/http"; @@ -10,6 +10,8 @@ import {HttpEventType, HttpProgressEvent, HttpResponse} from "@angular/common/ht export class BackgroundTaskService { private snackBarRef?: MatSnackBarRef; + private readonly scheduledTasks = new Map>(); + private readonly runningTasks = new Map>(); constructor(private snackBar: MatSnackBar) { } @@ -61,6 +63,31 @@ export class BackgroundTaskService { return true; } + schedule(taskName: string, task: () => Observable): Observable { + let alreadyScheduledTask = this.scheduledTasks.get(taskName); + if (alreadyScheduledTask) { + return alreadyScheduledTask; + } + + let alreadyRunningTask = this.runningTasks.get(taskName); + if (!alreadyRunningTask) { + // Initialize the running task with an already finished one for simplicity + alreadyRunningTask = of(undefined); + } + let scheduledTask = alreadyRunningTask.pipe(mergeMap(() => { + let runningTask = task() + // Multicast the result to all future subscribers since we don't want to rerun the task once for each subscriber + .pipe(share()); + this.runningTasks.set(taskName, runningTask); + this.scheduledTasks.delete(taskName); + return runningTask; + }), + // Multicast the result to all future subscribers since we don't want to rerun the task once for each subscriber + share()); + this.scheduledTasks.set(taskName, scheduledTask); + return scheduledTask; + } + private showSnackBar() { if (!this.snackBarRef) { this.snackBarRef = this.snackBar.openFromComponent(SnackBarProgressIndicatorComponent); @@ -136,6 +163,7 @@ class SnackBarProgressIndicatorComponent { }) } + isAllTaskFinished() { return this.dataList.every(this.isFinished); } From 21e81ba68ad776c70cde81683c5efb74e253e7a8 Mon Sep 17 00:00:00 2001 From: Musholic Date: Fri, 16 Feb 2024 11:14:49 +0100 Subject: [PATCH 62/66] [us40] Add eslint support to detect some unbound-method issues + add call to scheduleBakup and scheduleRunAll --- .eslintrc.js | 33 + .idea/inspectionProfiles/Project_Default.xml | 1 + package-lock.json | 1360 ++++++++++++++--- package.json | 5 +- .../background-task.service.spec.ts | 25 +- .../background-task.service.ts | 28 +- ...atabase-backup-and-restore.service.spec.ts | 43 - .../database-backup-and-restore.service.ts | 15 +- src/app/rules/rule.repository.spec.ts | 7 +- src/app/rules/rule.repository.ts | 4 +- src/app/rules/rule.service.spec.ts | 3 +- src/app/rules/rule.service.ts | 5 +- src/app/user-root/user-root.component.spec.ts | 7 +- src/app/user-root/user-root.component.ts | 2 +- 14 files changed, 1273 insertions(+), 265 deletions(-) create mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..7e713b3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended-type-checked" + ], + "overrides": [ + { + "env": { + "node": true + }, + "files": [ + ".eslintrc.{js,cjs}" + ], + "parserOptions": { + "sourceType": "script" + } + } + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": true + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": {} +} diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e9df7e0..45819fb 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,7 @@