Browse Source

Feature/support country codes in entity logo component (#7884)

* Add support for country codes to entity logo component

* Update changelog
pull/7888/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
e7f57d6dc0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 20
      libs/common/src/lib/helper.spec.ts
  3. 17
      libs/common/src/lib/helper.ts
  4. 5
      libs/ui/src/lib/account-selector/account-selector.component.html
  5. 13
      libs/ui/src/lib/currency-selector/currency-selector.component.html
  6. 15
      libs/ui/src/lib/currency-selector/currency-selector.component.ts
  7. 17
      libs/ui/src/lib/entity-logo/entity-logo.component.html
  8. 12
      libs/ui/src/lib/entity-logo/entity-logo.component.scss
  9. 8
      libs/ui/src/lib/entity-logo/entity-logo.component.stories.ts
  10. 12
      libs/ui/src/lib/entity-logo/entity-logo.component.ts
  11. 1
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  12. 2
      libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts
  13. 12
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  14. 46
      libs/ui/src/lib/mocks/holdings.ts

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a hint for the performance calculation type to the analysis page
- Added support for country codes to the entity logo component and used it for cash holdings and currency selectors
### Changed

20
libs/common/src/lib/helper.spec.ts

@ -6,6 +6,7 @@ import {
canApplyFiltersToAccess,
extractNumberFromString,
getCountryCodeFromCurrency,
getEmojiFlag,
getNumberFormatGroup,
getStringOrNull,
getStringOrUndefined,
@ -122,6 +123,25 @@ describe('Helper', () => {
});
});
describe('Get emoji flag', () => {
it('Country code', () => {
expect(getEmojiFlag('US')).toEqual('🇺🇸');
expect(getEmojiFlag('us')).toEqual('🇺🇸');
});
it('European Union code', () => {
expect(getEmojiFlag('EU')).toEqual('🇪🇺');
});
it('Invalid country code', () => {
expect(getEmojiFlag('XX')).toEqual(undefined);
});
it('Empty country code', () => {
expect(getEmojiFlag('')).toEqual(undefined);
});
});
describe('Get number format group', () => {
let languageGetter: jest.SpyInstance<string, [], any>;

17
libs/common/src/lib/helper.ts

@ -11,6 +11,7 @@ import {
} from '@prisma/client';
import { Big } from 'big.js';
import { isISO4217CurrencyCode, isUUID } from 'class-validator';
import { countries } from 'countries-list';
import {
addDays,
getDate,
@ -88,6 +89,8 @@ const USER_SETTINGS_KEYS_OF_AUTHENTICATED_USER: (keyof UserSettings)[] = [
'viewMode'
];
const VALID_COUNTRY_CODES = new Set([...Object.keys(countries), 'EU']);
export function applyAssetProfileOverrides<T extends Partial<SymbolProfile>>(
assetProfile: T,
assetProfileOverrides: AssetProfileOverrides | null
@ -439,15 +442,15 @@ export function getDateWithTimeFormatString(aLocale?: string) {
}
export function getEmojiFlag(aCountryCode: string) {
if (!aCountryCode) {
return aCountryCode;
const countryCode = aCountryCode?.toUpperCase();
if (!countryCode || !VALID_COUNTRY_CODES.has(countryCode)) {
return undefined;
}
return aCountryCode
.toUpperCase()
.replace(/./g, (character) =>
String.fromCodePoint(127397 + character.charCodeAt(0))
);
return countryCode.replace(/./g, (character) => {
return String.fromCodePoint(127397 + character.charCodeAt(0));
});
}
export function getLocale() {

5
libs/ui/src/lib/account-selector/account-selector.component.html

@ -11,11 +11,10 @@
(closed)="onPanelClosed()"
>
<mat-select-trigger>
<div class="d-flex">
@if (selectedAccount()) {
<div class="align-items-center align-middle d-inline-flex line-height-1">
@if (selectedAccount()?.platform?.url) {
<gf-entity-logo
class="mr-1"
[hasPlaceholder]="true"
[tooltip]="selectedAccount()?.platform?.name ?? ''"
[url]="selectedAccount()?.platform?.url ?? ''"
/>

13
libs/ui/src/lib/currency-selector/currency-selector.component.html

@ -1,5 +1,8 @@
@if (emojiFlagOfSelectedCurrency) {
<span class="flex-shrink-0 mr-1">{{ emojiFlagOfSelectedCurrency }}</span>
@if (countryCodeOfSelectedCurrency) {
<gf-entity-logo
class="flex-shrink-0 mr-1"
[countryCode]="countryCodeOfSelectedCurrency"
/>
}
<input
@ -19,7 +22,11 @@
@for (currency of filteredCurrencies; track currency) {
<mat-option class="line-height-1" [value]="currency">
<span class="align-items-center d-flex">
<span class="mr-1">{{ getEmojiFlagFromCurrency(currency) }}</span>
<gf-entity-logo
class="mr-1"
[countryCode]="getCountryCodeFromCurrency(currency)"
[hasPlaceholder]="true"
/>
<span>{{ currency }}</span>
</span>
</mat-option>

15
libs/ui/src/lib/currency-selector/currency-selector.component.ts

@ -1,7 +1,4 @@
import {
getCountryCodeFromCurrency,
getEmojiFlag
} from '@ghostfolio/common/helper';
import { getCountryCodeFromCurrency } from '@ghostfolio/common/helper';
import { FocusMonitor } from '@angular/cdk/a11y';
import {
@ -40,6 +37,7 @@ import {
import { MatInput, MatInputModule } from '@angular/material/input';
import { map, startWith } from 'rxjs/operators';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
@Component({
@ -51,6 +49,7 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
},
imports: [
FormsModule,
GfEntityLogoComponent,
MatAutocompleteModule,
MatFormFieldModule,
MatInputModule,
@ -99,12 +98,12 @@ export class GfCurrencySelectorComponent
return { elementRef: this.formField.getConnectedOverlayOrigin() };
}
public get emojiFlagOfSelectedCurrency() {
public get countryCodeOfSelectedCurrency() {
const selectedCurrency = this.currencies().find((currency) => {
return currency === this.control.value;
});
return this.getEmojiFlagFromCurrency(selectedCurrency);
return getCountryCodeFromCurrency(selectedCurrency);
}
public override get empty() {
@ -126,8 +125,8 @@ export class GfCurrencySelectorComponent
this.input().focus();
}
public getEmojiFlagFromCurrency(aCurrency = '') {
return getEmojiFlag(getCountryCodeFromCurrency(aCurrency));
public getCountryCodeFromCurrency(aCurrency = '') {
return getCountryCodeFromCurrency(aCurrency);
}
public ngOnInit() {

17
libs/ui/src/lib/entity-logo/entity-logo.component.html

@ -1,4 +1,19 @@
@if (src && !hasError) {
@if (emojiFlag) {
<span
class="cursor-default d-block logo overflow-hidden position-relative"
role="img"
[attr.aria-label]="tooltip ? tooltip : countryCode"
[class.large]="size === 'large'"
[title]="tooltip ? tooltip : ''"
>
<span
aria-hidden="true"
class="align-items-center d-flex emoji justify-content-center line-height-1 position-absolute"
>
{{ emojiFlag }}
</span>
</span>
} @else if (src && !hasError) {
<img
class="logo"
[class.large]="size === 'large'"

12
libs/ui/src/lib/entity-logo/entity-logo.component.scss

@ -1,7 +1,4 @@
:host {
align-items: center;
display: flex;
.logo {
border-radius: 0.2rem;
height: 0.8rem;
@ -10,6 +7,15 @@
&.large {
height: 1.4rem;
width: 1.4rem;
.emoji {
font-size: 1.9rem;
}
}
.emoji {
font-size: 1.1rem;
inset: 0;
}
}
}

8
libs/ui/src/lib/entity-logo/entity-logo.component.stories.ts

@ -28,6 +28,14 @@ export default {
type Story = StoryObj<GfEntityLogoComponent>;
export const FlagByCountryCode: Story = {
args: {
countryCode: 'US',
size: 'large',
tooltip: 'United States'
}
};
export const LogoByAssetProfileIdentifier: Story = {
args: {
dataSource: 'YAHOO',

12
libs/ui/src/lib/entity-logo/entity-logo.component.ts

@ -1,3 +1,4 @@
import { getEmojiFlag } from '@ghostfolio/common/helper';
import { EntityLogoImageSourceService } from '@ghostfolio/ui/entity-logo/entity-logo-image-source.service';
import {
@ -11,12 +12,14 @@ import { DataSource } from '@prisma/client';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'align-items-center d-flex' },
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-entity-logo',
styleUrls: ['./entity-logo.component.scss'],
templateUrl: './entity-logo.component.html'
})
export class GfEntityLogoComponent implements OnChanges {
@Input() countryCode: string;
@Input() dataSource: DataSource;
@Input() hasPlaceholder = false;
@Input() size: 'large';
@ -24,6 +27,7 @@ export class GfEntityLogoComponent implements OnChanges {
@Input() tooltip: string;
@Input() url: string;
public emojiFlag?: string;
public hasError = false;
public src?: string;
@ -32,17 +36,19 @@ export class GfEntityLogoComponent implements OnChanges {
) {}
public ngOnChanges() {
this.emojiFlag = undefined;
this.hasError = false;
this.src = undefined;
if (this.dataSource && this.symbol) {
if (this.countryCode) {
this.emojiFlag = getEmojiFlag(this.countryCode);
} else if (this.dataSource && this.symbol) {
this.src = this.imageSourceService.getLogoUrlByAssetProfileIdentifier({
dataSource: this.dataSource,
symbol: this.symbol
});
} else if (this.url) {
this.src = this.imageSourceService.getLogoUrlByUrl(this.url);
} else {
this.src = undefined;
}
}

1
libs/ui/src/lib/holdings-table/holdings-table.component.html

@ -17,6 +17,7 @@
[class.text-nowrap]="mode() === 'simple'"
>
<gf-entity-logo
[countryCode]="getCountryCodeForCashPosition(element)"
[dataSource]="element.assetProfile.dataSource"
[symbol]="element.assetProfile.symbol"
[tooltip]="element.assetProfile.name"

2
libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts

@ -45,10 +45,10 @@ type Story = StoryObj<GfHoldingsTableComponent>;
export const Loading: Story = {
args: {
holdings: undefined,
hasPermissionToOpenDetails: false,
hasPermissionToShowQuantities: true,
hasPermissionToShowValues: true,
holdings: undefined,
locale: 'en-US',
pageSize: Number.MAX_SAFE_INTEGER
}

12
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -1,7 +1,9 @@
import {
canOpenHoldingDetail,
getCountryCodeFromCurrency,
getLocale,
getLowercase
getLowercase,
isCashPosition
} from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
@ -120,6 +122,14 @@ export class GfHoldingsTableComponent {
return this.hasPermissionToOpenDetails() && canOpenHoldingDetail(holding);
}
protected getCountryCodeForCashPosition({
assetProfile
}: PortfolioPosition): string {
return isCashPosition(assetProfile)
? getCountryCodeFromCurrency(assetProfile.currency)
: '';
}
protected onOpenHoldingDialog({
dataSource,
symbol

46
libs/ui/src/lib/mocks/holdings.ts

@ -3,7 +3,7 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export const holdings: PortfolioPosition[] = [
{
activitiesCount: 1,
allocationInPercentage: 0.042990776363386086,
allocationInPercentage: 0.06447799495516787,
assetProfile: {
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
@ -48,7 +48,7 @@ export const holdings: PortfolioPosition[] = [
},
{
activitiesCount: 2,
allocationInPercentage: 0.02377401948293552,
allocationInPercentage: 0.03565651141834935,
assetProfile: {
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
@ -93,7 +93,7 @@ export const holdings: PortfolioPosition[] = [
},
{
activitiesCount: 1,
allocationInPercentage: 0.08038536990007467,
allocationInPercentage: 0.12056277911976934,
assetProfile: {
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
@ -138,7 +138,7 @@ export const holdings: PortfolioPosition[] = [
},
{
activitiesCount: 1,
allocationInPercentage: 0.19216416482928922,
allocationInPercentage: 0.28820973005222217,
assetProfile: {
assetClass: 'LIQUIDITY',
assetClassLabel: 'Liquidity',
@ -171,7 +171,7 @@ export const holdings: PortfolioPosition[] = [
},
{
activitiesCount: 1,
allocationInPercentage: 0.04307127421937313,
allocationInPercentage: 0.06459872644204223,
assetProfile: {
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
@ -216,7 +216,7 @@ export const holdings: PortfolioPosition[] = [
},
{
activitiesCount: 1,
allocationInPercentage: 0.18762679306394897,
allocationInPercentage: 0.28140453464652826,
assetProfile: {
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
@ -261,7 +261,7 @@ export const holdings: PortfolioPosition[] = [
},
{
activitiesCount: 5,
allocationInPercentage: 0.053051250766657634,
allocationInPercentage: 0.0795667947557967,
assetProfile: {
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
@ -303,5 +303,37 @@ export const holdings: PortfolioPosition[] = [
quantity: 50,
tags: [],
valueInBaseCurrency: 15092
},
{
activitiesCount: 3,
allocationInPercentage: 0.06552292861012407,
assetProfile: {
assetClass: 'LIQUIDITY',
assetClassLabel: 'Liquidity',
assetSubClass: 'CASH',
assetSubClassLabel: 'Cash',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
holdings: [],
name: 'USD',
sectors: [],
symbol: 'USD'
},
dateOfFirstActivity: new Date('2024-01-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 0,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0,
grossPerformanceWithCurrencyEffect: 0,
investment: 12428.2,
marketPrice: 1,
netPerformance: 0,
netPerformancePercent: 0,
netPerformancePercentWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
quantity: 12428.2,
tags: [],
valueInBaseCurrency: 12428.2
}
];

Loading…
Cancel
Save