diff --git a/CHANGELOG.md b/CHANGELOG.md index ad60933ec..9edeeebb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Extended the feature overview page by liabilities + +### Changed + +- Extracted the symbol search to a dedicated component +- Improved the column headers in the holdings table for mobile +- Upgraded `prisma` from version `4.14.1` to `4.15.0` + ## 1.280.1 - 2023-06-10 ### Added diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html index 961bbe9cb..d898cfca0 100644 --- a/apps/client/src/app/pages/features/features-page.html +++ b/apps/client/src/app/pages/features/features-page.html @@ -1,7 +1,7 @@
-

Features

+

Features

Check out the numerous features of Ghostfolio to @@ -13,7 +13,7 @@

-

Stocks

+

Stocks

Keep track of your stock purchases and sales.

@@ -23,7 +23,7 @@
-

ETFs

+

ETFs

Are you into ETFs (Exchange Traded Funds)? Track your ETF investments. @@ -36,7 +36,7 @@

-

Bonds

+

Bonds

Manage your investment in bonds and other assets with fixed income. @@ -49,7 +49,7 @@

-

Cryptocurrencies

+

Cryptocurrencies

Keep track of your Bitcoin and Altcoin holdings.

@@ -61,7 +61,7 @@
-

Dividend

+

Dividend

Are you building a dividend portfolio? Track your dividend in Ghostfolio. @@ -74,7 +74,7 @@

-

Wealth Items

+

Wealth Items

Track all your treasuries, be it your luxury watch or rare trading cards. @@ -87,7 +87,7 @@

-

Emergency Fund

+

Emergency Fund

Define your emergency fund you are comfortable with for difficult times. @@ -100,7 +100,22 @@

-

Import and Export

+

Liabilities

+

+ Manage your financial liabilities, such as your student loan, + to stay ahead of your financial obligations. +

+
+ + +
+
+ + +
+

+ Import and Export +

Import and export your investment activities.

@@ -110,7 +125,7 @@
-

Multi-Accounts

+

Multi-Accounts

Keep an eye on all your accounts across multiple platforms (multi-banking). @@ -124,7 +139,7 @@

- Portfolio Calculations + Portfolio Calculations

- Portfolio Allocations + Portfolio Allocations
-

Dark Mode

+

Dark Mode

Ghostfolio automatically switches to a dark color theme based on your operating system's preferences. @@ -175,7 +190,7 @@

-

Zen Mode

+

Zen Mode

Keep calm and activate Zen Mode if the markets are going crazy. @@ -192,7 +207,7 @@

- Market Mood + Market Mood

@@ -210,7 +225,7 @@

- Static Analysis + Static Analysis
-

Multi-Language

+

Multi-Language

Use Ghostfolio in multiple languages: English, Dutch, French, German, Italian

-

Community

+

Community

Join the Ghostfolio

diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index d9fdb3fe4..6962d4d40 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -55,8 +55,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { public currencies: string[] = []; public currentMarketPrice = null; public defaultDateFormat: string; - public filteredLookupItems: LookupItem[] = []; - public filteredLookupItemsObservable: Observable = of([]); public filteredTagsObservable: Observable = of([]); public isLoading = false; public platforms: { id: string; name: string }[]; @@ -120,10 +118,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { name: [this.data.activity?.SymbolProfile?.name, Validators.required], quantity: [this.data.activity?.quantity, Validators.required], searchSymbol: [ - { - dataSource: this.data.activity?.SymbolProfile?.dataSource, - symbol: this.data.activity?.SymbolProfile?.symbol - }, + !!this.data.activity?.SymbolProfile + ? { + dataSource: this.data.activity?.SymbolProfile?.dataSource, + symbol: this.data.activity?.SymbolProfile?.symbol + } + : null, Validators.required ], tags: [ @@ -238,28 +238,19 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.changeDetectorRef.markForCheck(); }); - this.filteredLookupItemsObservable = this.activityForm.controls[ - 'searchSymbol' - ].valueChanges.pipe( - debounceTime(400), - distinctUntilChanged(), - switchMap((query: string) => { - if (isString(query) && query.length > 1) { - const filteredLookupItemsObservable = - this.dataService.fetchSymbols(query); - - filteredLookupItemsObservable - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((filteredLookupItems) => { - this.filteredLookupItems = filteredLookupItems; - }); + this.activityForm.controls['searchSymbol'].valueChanges.subscribe(() => { + if (this.activityForm.controls['searchSymbol'].invalid) { + this.data.activity.SymbolProfile = null; + } else { + this.activityForm.controls['dataSource'].setValue( + this.activityForm.controls['searchSymbol'].value.dataSource + ); - return filteredLookupItemsObservable; - } + this.updateSymbol(); + } - return []; - }) - ); + this.changeDetectorRef.markForCheck(); + }); this.filteredTagsObservable = this.activityForm.controls[ 'tags' @@ -393,25 +384,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.tagInput.nativeElement.value = ''; } - public onBlurSymbol() { - const currentLookupItem = this.filteredLookupItems.find((lookupItem) => { - return ( - lookupItem.symbol === - this.activityForm.controls['searchSymbol'].value.symbol - ); - }); - - if (currentLookupItem) { - this.updateSymbol(currentLookupItem.symbol); - } else { - this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true }); - - this.data.activity.SymbolProfile = null; - } - - this.changeDetectorRef.markForCheck(); - } - public onCancel() { this.dialogRef.close(); } @@ -455,13 +427,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.dialogRef.close({ activity }); } - public onUpdateSymbol(event: MatAutocompleteSelectedEvent) { - this.activityForm.controls['dataSource'].setValue( - event.option.value.dataSource - ); - this.updateSymbol(event.option.value.symbol); - } - public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); @@ -477,12 +442,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { }); } - private updateSymbol(symbol: string) { + private updateSymbol() { this.isLoading = true; - - this.activityForm.controls['searchSymbol'].setErrors(null); - this.activityForm.controls['searchSymbol'].setValue({ symbol }); - this.changeDetectorRef.markForCheck(); this.dataService diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 85511f012..a78b6ba86 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -48,34 +48,10 @@ > Name, symbol or ISIN - - - - {{ lookupItem.name }} -
- {{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency - }} -
-
-
Higher Risk - Risco mais Elevado + Risco mais Elevado libs/ui/src/lib/i18n.ts 13 @@ -3251,7 +3251,7 @@ Lower Risk - Risco menos Elevado + Risco menos Elevado libs/ui/src/lib/i18n.ts 15 @@ -3991,4 +3991,4 @@ - \ No newline at end of file + diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.html b/libs/ui/src/lib/holdings-table/holdings-table.component.html index 9bda316c4..5be58e663 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.html +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -92,7 +92,8 @@ mat-header-cell mat-sort-header > - Allocation + Allocation + %
@@ -108,17 +109,14 @@ - Performance + Performance + ± - +
+ implements ControlValueAccessor, DoCheck, MatFormFieldControl, OnDestroy +{ + @HostBinding() + public id = `${this.controlType}-${AbstractMatFormField.nextId++}`; + + @HostBinding('attr.aria-describedBy') public describedBy = ''; + + public readonly autofilled: boolean; + public errorState: boolean; + public focused = false; + public readonly stateChanges = new Subject(); + public readonly userAriaDescribedBy: string; + + protected onChange?: (value: T) => void; + protected onTouched?: () => void; + + private static nextId: number = 0; + + protected constructor( + protected _elementRef: ElementRef, + protected _focusMonitor: FocusMonitor, + public readonly ngControl: NgControl + ) { + if (this.ngControl) { + this.ngControl.valueAccessor = this; + } + + _focusMonitor + .monitor(this._elementRef.nativeElement, true) + .subscribe((origin) => { + this.focused = !!origin; + this.stateChanges.next(); + }); + } + + private _controlType: string; + + public get controlType(): string { + return this._controlType; + } + + protected set controlType(value: string) { + this._controlType = value; + this.id = `${this._controlType}-${AbstractMatFormField.nextId++}`; + } + + private _value: T; + + public get value(): T { + return this._value; + } + + public set value(value: T) { + this._value = value; + + if (this.onChange) { + this.onChange(value); + } + } + + public get empty(): boolean { + return !this._value; + } + + public _placeholder: string = ''; + + public get placeholder() { + return this._placeholder; + } + + @Input() + public set placeholder(placeholder: string) { + this._placeholder = placeholder; + this.stateChanges.next(); + } + + public _required: boolean = false; + + public get required() { + return this._required; + } + + @Input() + public set required(required: any) { + this._required = coerceBooleanProperty(required); + this.stateChanges.next(); + } + + public _disabled: boolean = false; + + public get disabled() { + if (this.ngControl && this.ngControl.disabled !== null) { + return this.ngControl.disabled; + } + + return this._disabled; + } + + @Input() + public set disabled(disabled: any) { + this._disabled = coerceBooleanProperty(disabled); + + if (this.focused) { + this.focused = false; + this.stateChanges.next(); + } + } + + public abstract focus(): void; + + public get shouldLabelFloat(): boolean { + return this.focused || !this.empty; + } + + public ngDoCheck(): void { + if (this.ngControl) { + this.errorState = this.ngControl.invalid && this.ngControl.touched; + this.stateChanges.next(); + } + } + + public ngOnDestroy(): void { + this.stateChanges.complete(); + this._focusMonitor.stopMonitoring(this._elementRef.nativeElement); + } + + public registerOnChange(fn: (_: T) => void): void { + this.onChange = fn; + } + + public registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + public setDescribedByIds(ids: string[]): void { + this.describedBy = ids.join(' '); + } + + public writeValue(value: T): void { + this.value = value; + } + + @HostListener('focusout') + public onBlur() { + this.focused = false; + + if (this.onTouched) { + this.onTouched(); + } + + this.stateChanges.next(); + } + + public onContainerClick(): void { + if (!this.focused) { + this.focus(); + } + } +} diff --git a/libs/ui/src/lib/symbol-autocomplete/index.ts b/libs/ui/src/lib/symbol-autocomplete/index.ts new file mode 100644 index 000000000..7271d1ca9 --- /dev/null +++ b/libs/ui/src/lib/symbol-autocomplete/index.ts @@ -0,0 +1 @@ +export * from './symbol-autocomplete.module'; diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html new file mode 100644 index 000000000..47cb8bcb4 --- /dev/null +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html @@ -0,0 +1,34 @@ + + + + + + {{ lookupItem.name }} +
+ {{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency }} +
+
+
+ diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.scss b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.scss new file mode 100644 index 000000000..71c06f26e --- /dev/null +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.scss @@ -0,0 +1,8 @@ +:host { + display: block; + + .mat-mdc-progress-spinner { + right: 0; + top: calc(50% - 10px); + } +} diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts new file mode 100644 index 000000000..2b6bc45ca --- /dev/null +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -0,0 +1,169 @@ +import { FocusMonitor } from '@angular/cdk/a11y'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { FormControl, NgControl, Validators } from '@angular/forms'; +import { + MatAutocomplete, + MatAutocompleteSelectedEvent +} from '@angular/material/autocomplete'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { isString } from 'lodash'; +import { Observable, Subject, of, tap } from 'rxjs'; +import { + debounceTime, + distinctUntilChanged, + filter, + switchMap +} from 'rxjs/operators'; + +import { AbstractMatFormField } from './abstract-mat-form-field'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[attr.aria-describedBy]': 'describedBy', + '[id]': 'id' + }, + selector: 'gf-symbol-autocomplete', + styleUrls: ['./symbol-autocomplete.component.scss'], + templateUrl: 'symbol-autocomplete.component.html', + providers: [ + { + provide: MatFormFieldControl, + useExisting: SymbolAutocompleteComponent + } + ] +}) +export class SymbolAutocompleteComponent + extends AbstractMatFormField + implements OnInit, OnDestroy +{ + @Input() public isLoading = false; + + @ViewChild(MatInput, { static: false }) private input: MatInput; + + @ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete; + + public control = new FormControl(); + public filteredLookupItems: LookupItem[] = []; + public filteredLookupItemsObservable: Observable = of([]); + + private unsubscribeSubject = new Subject(); + + public constructor( + public readonly _elementRef: ElementRef, + public readonly _focusMonitor: FocusMonitor, + public readonly changeDetectorRef: ChangeDetectorRef, + public readonly dataService: DataService, + public readonly ngControl: NgControl + ) { + super(_elementRef, _focusMonitor, ngControl); + + this.controlType = 'symbol-autocomplete'; + } + + public ngOnInit() { + super.required = this.ngControl.control?.hasValidator(Validators.required); + + if (this.disabled) { + this.control.disable(); + } + + this.control.valueChanges + .pipe( + debounceTime(400), + distinctUntilChanged(), + filter((query) => { + return isString(query) && query.length > 1; + }), + tap(() => { + this.isLoading = true; + this.changeDetectorRef.markForCheck(); + }), + switchMap((query: string) => { + return this.dataService.fetchSymbols(query); + }) + ) + .subscribe((filteredLookupItems) => { + this.filteredLookupItems = filteredLookupItems; + this.isLoading = false; + + this.changeDetectorRef.markForCheck(); + }); + } + + public displayFn(aLookupItem: LookupItem) { + return aLookupItem?.symbol ?? ''; + } + + public get empty() { + return this.input?.empty; + } + + public focus() { + this.input.focus(); + } + + public isValueInOptions(value: string) { + return this.filteredLookupItems.some((item) => { + return item.symbol === value; + }); + } + + public ngDoCheck() { + if (this.ngControl) { + this.validateRequired(); + this.validateSelection(); + this.errorState = this.ngControl.invalid && this.ngControl.touched; + this.stateChanges.next(); + } + } + + public onUpdateSymbol(event: MatAutocompleteSelectedEvent) { + super.value = { + dataSource: event.option.value.dataSource, + symbol: event.option.value.symbol + } as LookupItem; + } + + public set value(value: LookupItem) { + this.control.setValue(value); + super.value = value; + } + + public ngOnDestroy() { + super.ngOnDestroy(); + + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private validateRequired() { + const requiredCheck = super.required + ? !super.value?.dataSource || !super.value?.symbol + : false; + if (requiredCheck) { + this.ngControl.control.setErrors({ invalidData: true }); + } + } + + private validateSelection() { + const error = + !this.isValueInOptions(this.input?.value) || + this.input?.value !== super.value?.symbol; + if (error) { + this.ngControl.control.setErrors({ invalidData: true }); + } + } +} diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.module.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.module.ts new file mode 100644 index 000000000..d7b1ed2f8 --- /dev/null +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.module.ts @@ -0,0 +1,26 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; +import { SymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.component'; + +@NgModule({ + declarations: [SymbolAutocompleteComponent], + exports: [SymbolAutocompleteComponent], + imports: [ + CommonModule, + FormsModule, + GfSymbolModule, + MatAutocompleteModule, + MatFormFieldModule, + MatInputModule, + MatProgressSpinnerModule, + ReactiveFormsModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfSymbolAutocompleteModule {} diff --git a/package.json b/package.json index c328e2a1b..c49046bfc 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@nestjs/platform-express": "9.1.4", "@nestjs/schedule": "2.1.0", "@nestjs/serve-static": "3.0.0", - "@prisma/client": "4.14.1", + "@prisma/client": "4.15.0", "@simplewebauthn/browser": "5.2.1", "@simplewebauthn/server": "5.2.1", "@stripe/stripe-js": "1.47.0", @@ -119,7 +119,7 @@ "passport": "0.6.0", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "4.14.1", + "prisma": "4.15.0", "reflect-metadata": "0.1.13", "rxjs": "7.5.6", "stripe": "11.12.0", diff --git a/yarn.lock b/yarn.lock index 224e66b60..9ce071222 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3936,22 +3936,22 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@prisma/client@4.14.1": - version "4.14.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.14.1.tgz#61720f385f687f7e88de41fccade1ed62be57a54" - integrity sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw== +"@prisma/client@4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.15.0.tgz#f52ec6ca6fbde37395a54b0a9e5da603a9de15f3" + integrity sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw== dependencies: - "@prisma/engines-version" "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c" + "@prisma/engines-version" "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944" -"@prisma/engines-version@4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c": - version "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz#0aeca447c4a5f23c83f68b8033e627b60bc01850" - integrity sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw== +"@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944": + version "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz#8d880becf996cffe08c78ad5afab6bc06090c990" + integrity sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg== -"@prisma/engines@4.14.1": - version "4.14.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.14.1.tgz#dac49f8d1f2d4f14a8ed7e6f96b24cd49bd6cd91" - integrity sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA== +"@prisma/engines@4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.15.0.tgz#d8687a9fda615fab88b75b466931280289de9e26" + integrity sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -15184,12 +15184,12 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== -prisma@4.14.1: - version "4.14.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.14.1.tgz#7a6bb4ce847a9d08deabb6acdf3116fff15e1316" - integrity sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g== +prisma@4.15.0: + version "4.15.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.15.0.tgz#4faa94f0d584828b68468953ff0bc88f37912c8c" + integrity sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA== dependencies: - "@prisma/engines" "4.14.1" + "@prisma/engines" "4.15.0" prismjs@^1.28.0: version "1.29.0"