diff --git a/CHANGELOG.md b/CHANGELOG.md index 49419da3c..17ff05e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for notes in the activities import - Added the application version to the endpoint `GET api/v1/admin` +- Introduced a carousel component for the testimonial section on the landing page + +### Changed + +- Displayed the link to the markets overview on the home page without any permission + +### Fixed + +- Fixed the style of the active features page in the navigation on desktop ## 2.8.0 - 2023-10-03 diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index 50a4f2b12..2f0399fb8 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -3,7 +3,7 @@ import { cloneDeep, isArray, isObject } from 'lodash'; export function hasNotDefinedValuesInObject(aObject: Object): boolean { for (const key in aObject) { - if (aObject[key] === null || aObject[key] === null) { + if (aObject[key] === null || aObject[key] === undefined) { return true; } else if (isObject(aObject[key])) { return hasNotDefinedValuesInObject(aObject[key]); diff --git a/apps/client/src/app/app.module.ts b/apps/client/src/app/app.module.ts index a5f0d755c..608ba0100 100644 --- a/apps/client/src/app/app.module.ts +++ b/apps/client/src/app/app.module.ts @@ -1,6 +1,6 @@ import { Platform } from '@angular/cdk/platform'; import { HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatChipsModule } from '@angular/material/chips'; import { @@ -35,6 +35,7 @@ export function NgxStripeFactory(): string { } @NgModule({ + bootstrap: [AppComponent], declarations: [AppComponent], imports: [ AppRoutingModule, @@ -72,6 +73,6 @@ export function NgxStripeFactory(): string { useFactory: NgxStripeFactory } ], - bootstrap: [AppComponent] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule {} diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index bef984729..792025e9b 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -13,7 +13,6 @@ import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { AdminMarketDataDetails, - ScraperConfiguration, UniqueAsset } from '@ghostfolio/common/interfaces'; import { translate } from '@ghostfolio/ui/i18n'; diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 2ca3f0724..b1e91dfc9 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -43,7 +43,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { public transactionCount: number; public userCount: number; public user: User; - public version = environment.version; + public version: string; private unsubscribeSubject = new Subject(); @@ -204,15 +204,18 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { this.adminService .fetchAdminData() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ exchangeRates, settings, transactionCount, userCount }) => { - this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? []; - this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[]; - this.exchangeRates = exchangeRates; - this.transactionCount = transactionCount; - this.userCount = userCount; - - this.changeDetectorRef.markForCheck(); - }); + .subscribe( + ({ exchangeRates, settings, transactionCount, userCount, version }) => { + this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? []; + this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[]; + this.exchangeRates = exchangeRates; + this.transactionCount = transactionCount; + this.userCount = userCount; + this.version = version; + + this.changeDetectorRef.markForCheck(); + } + ); } private generateCouponCode(aLength: number) { diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html index 2739547b5..47f27dd40 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -5,14 +5,16 @@
Version
-
{{ version }}
+
+ +
User Count
@@ -21,8 +23,8 @@
Activity Count
diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index 2c56cb742..a2c69245c 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -272,7 +272,7 @@ mat-flat-button [ngClass]="{ 'font-weight-bold': currentRoute === routeFeatures, - 'text-decoration-underline': currentRoute === routeFeatuers + 'text-decoration-underline': currentRoute === routeFeatures }" [routerLink]="routerLinkFeatures" >Features

Markets

-
+
Last {{ numberOfDays }} Days @@ -8,15 +8,15 @@
Sectors
@@ -225,11 +225,11 @@
Countries
diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index 69972c7db..2d068dde1 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -10,7 +10,11 @@
Name - +
@@ -26,7 +30,12 @@
Cash Balance - + {{ data.account.currency }}
@@ -66,7 +75,7 @@
- + + +
+ +
+ + diff --git a/libs/ui/src/lib/carousel/carousel.component.scss b/libs/ui/src/lib/carousel/carousel.component.scss new file mode 100644 index 000000000..38da7c100 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.component.scss @@ -0,0 +1,34 @@ +:host { + display: block; + position: relative; + + ::ng-deep { + [gf-carousel-item] { + flex-shrink: 0; + width: 100%; + } + } + + button { + top: 50%; + transform: translateY(-50%); + + &.carousel-nav-prev { + left: -50px; + } + + &.carousel-nav-next { + right: -50px; + } + } + + .carousel-content { + flex-direction: row; + outline: none; + transition: transform 0.5s ease-in-out; + + .animations-disabled & { + transition: none; + } + } +} diff --git a/libs/ui/src/lib/carousel/carousel.component.ts b/libs/ui/src/lib/carousel/carousel.component.ts new file mode 100644 index 000000000..a0eb0f8a1 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.component.ts @@ -0,0 +1,147 @@ +import { FocusKeyManager } from '@angular/cdk/a11y'; +import { LEFT_ARROW, RIGHT_ARROW, TAB } from '@angular/cdk/keycodes'; +import { + AfterContentInit, + ChangeDetectionStrategy, + Component, + ContentChildren, + ElementRef, + HostBinding, + Inject, + Input, + Optional, + QueryList, + ViewChild +} from '@angular/core'; +import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations'; + +import { CarouselItem } from './carousel-item.directive'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-carousel', + styleUrls: ['./carousel.component.scss'], + templateUrl: './carousel.component.html' +}) +export class CarouselComponent implements AfterContentInit { + @ContentChildren(CarouselItem) public items!: QueryList; + + @HostBinding('class.animations-disabled') + public readonly animationsDisabled: boolean; + + @Input('aria-label') public ariaLabel: string | undefined; + + @ViewChild('list') public list!: ElementRef; + + public showPrevArrow = false; + public showNextArrow = true; + + private index = 0; + private keyManager!: FocusKeyManager; + private position = 0; + + public constructor( + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationsModule?: string + ) { + this.animationsDisabled = animationsModule === 'NoopAnimations'; + } + + public ngAfterContentInit() { + this.keyManager = new FocusKeyManager(this.items); + } + + public next() { + for (let i = this.index; i < this.items.length; i++) { + if (this.isOutOfView(i)) { + this.index = i; + this.scrollToActiveItem(); + break; + } + } + } + + public onKeydown({ keyCode }: KeyboardEvent) { + const manager = this.keyManager; + const previousActiveIndex = manager.activeItemIndex; + + if (keyCode === LEFT_ARROW) { + manager.setPreviousItemActive(); + } else if (keyCode === RIGHT_ARROW) { + manager.setNextItemActive(); + } else if (keyCode === TAB && !manager.activeItem) { + manager.setFirstItemActive(); + } + + if ( + manager.activeItemIndex != null && + manager.activeItemIndex !== previousActiveIndex + ) { + this.index = manager.activeItemIndex; + this.updateItemTabIndices(); + + if (this.isOutOfView(this.index)) { + this.scrollToActiveItem(); + } + } + } + + public previous() { + for (let i = this.index; i > -1; i--) { + if (this.isOutOfView(i)) { + this.index = i; + this.scrollToActiveItem(); + break; + } + } + } + + private isOutOfView(index: number, side?: 'start' | 'end') { + const { offsetWidth, offsetLeft } = + this.items.toArray()[index].element.nativeElement; + + if ((!side || side === 'start') && offsetLeft - this.position < 0) { + return true; + } + + return ( + (!side || side === 'end') && + offsetWidth + offsetLeft - this.position > + this.list.nativeElement.clientWidth + ); + } + + private scrollToActiveItem() { + if (!this.isOutOfView(this.index)) { + return; + } + + const itemsArray = this.items.toArray(); + let targetItemIndex = this.index; + + if (this.index > 0 && !this.isOutOfView(this.index - 1)) { + targetItemIndex = + itemsArray.findIndex((_, i) => !this.isOutOfView(i)) + 1; + } + + this.position = + itemsArray[targetItemIndex].element.nativeElement.offsetLeft; + this.list.nativeElement.style.transform = `translateX(-${this.position}px)`; + this.showPrevArrow = this.index > 0; + this.showNextArrow = false; + + for (let i = itemsArray.length - 1; i > -1; i--) { + if (this.isOutOfView(i, 'end')) { + this.showNextArrow = true; + break; + } + } + } + + private updateItemTabIndices() { + this.items.forEach((item: CarouselItem) => { + if (this.keyManager != null) { + item.tabindex = item === this.keyManager.activeItem ? '0' : '-1'; + } + }); + } +} diff --git a/libs/ui/src/lib/carousel/carousel.module.ts b/libs/ui/src/lib/carousel/carousel.module.ts new file mode 100644 index 000000000..4e43f23b0 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; + +import { CarouselItem } from './carousel-item.directive'; +import { CarouselComponent } from './carousel.component'; + +@NgModule({ + declarations: [CarouselComponent, CarouselItem], + exports: [CarouselComponent, CarouselItem], + imports: [CommonModule, MatButtonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfCarouselModule {} diff --git a/libs/ui/src/lib/carousel/index.ts b/libs/ui/src/lib/carousel/index.ts new file mode 100644 index 000000000..2e039a80b --- /dev/null +++ b/libs/ui/src/lib/carousel/index.ts @@ -0,0 +1 @@ +export * from './carousel.module'; diff --git a/test/import/ok.csv b/test/import/ok.csv index 732ab4699..9f1f1c768 100644 --- a/test/import/ok.csv +++ b/test/import/ok.csv @@ -1,5 +1,5 @@ -Date,Code,Currency,Price,Quantity,Action,Fee -16-09-2021,MSFT,USD,298.580,5,buy,19.00 +Date,Code,Currency,Price,Quantity,Action,Fee,Note +16-09-2021,MSFT,USD,298.580,5,buy,19.00,My first order 🤓 17/11/2021,MSFT,USD,0.62,5,dividend,0.00 01.01.2022,Penthouse Apartment,USD,500000.0,1,item,0.00 20500606,MSFT,USD,0.00,0,buy,0.00 diff --git a/test/import/ok.json b/test/import/ok.json index e488c4dc4..8803c9d08 100644 --- a/test/import/ok.json +++ b/test/import/ok.json @@ -52,7 +52,7 @@ }, { "accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0", - "comment": "My first order", + "comment": "My first order 🤓", "fee": 19, "quantity": 5, "type": "BUY",