Merge branch 'main' of github.com:ghostfolio/ghostfolio
All checks were successful
Docker image CD / build_and_push (push) Successful in 36m31s
All checks were successful
Docker image CD / build_and_push (push) Successful in 36m31s
This commit is contained in:
commit
1c604c0dbe
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Asia-Pacific Markets)
|
||||
- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan)
|
||||
- Added support to create custom tags in the holding detail dialog
|
||||
- Extended the tags selector component by a `readonly` attribute
|
||||
- Extended the tags selector component to support creating custom tags
|
||||
- Extended the holding detail dialog by the historical market data editor (experimental)
|
||||
@ -19,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `@trivago/prettier-plugin-sort-imports` from version `5.2.1` to `5.2.2`
|
||||
|
||||
## 2.138.0 - 2025-02-08
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateTagDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
|
||||
import {
|
||||
Body,
|
||||
@ -8,11 +9,13 @@ import {
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
@ -23,7 +26,10 @@ import { UpdateTagDto } from './update-tag.dto';
|
||||
|
||||
@Controller('tag')
|
||||
export class TagController {
|
||||
public constructor(private readonly tagService: TagService) {}
|
||||
public constructor(
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly tagService: TagService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@HasPermission(permissions.readTags)
|
||||
@ -33,9 +39,34 @@ export class TagController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HasPermission(permissions.createTag)
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async createTag(@Body() data: CreateTagDto): Promise<Tag> {
|
||||
const canCreateOwnTag = hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.createOwnTag
|
||||
);
|
||||
|
||||
const canCreateTag = hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.createTag
|
||||
);
|
||||
|
||||
if (!canCreateOwnTag && !canCreateTag) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
if (canCreateOwnTag && !canCreateTag) {
|
||||
if (data.userId !== this.request.user.id) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
||||
StatusCodes.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.tagService.createTag(data);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateTagDto {
|
||||
@IsString()
|
||||
@ -6,4 +6,8 @@ export class UpdateTagDto {
|
||||
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
}
|
||||
|
@ -347,6 +347,7 @@ export class UserService {
|
||||
permissions.accessHoldingsChart,
|
||||
permissions.createAccess,
|
||||
permissions.createMarketDataOfOwnAssetProfile,
|
||||
permissions.createOwnTag,
|
||||
permissions.readAiPrompt,
|
||||
permissions.readMarketDataOfOwnAssetProfile,
|
||||
permissions.updateMarketDataOfOwnAssetProfile
|
||||
|
@ -2,6 +2,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
|
||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { NUMERICAL_PRECISION_THRESHOLD } from '@ghostfolio/common/config';
|
||||
@ -50,7 +51,7 @@ import { Account, MarketData, Tag } from '@prisma/client';
|
||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { HoldingDetailDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@ -98,6 +99,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
public dividendYieldPercentWithCurrencyEffect: number;
|
||||
public feeInBaseCurrency: number;
|
||||
public firstBuyDate: string;
|
||||
public hasPermissionToCreateOwnTag: boolean;
|
||||
public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public investment: number;
|
||||
@ -131,6 +133,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>,
|
||||
@ -153,15 +156,40 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
this.activityForm
|
||||
.get('tags')
|
||||
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((tags) => {
|
||||
this.dataService
|
||||
.putHoldingTags({
|
||||
tags,
|
||||
dataSource: this.data.dataSource,
|
||||
symbol: this.data.symbol
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
.subscribe((tags: Tag[]) => {
|
||||
const newTag = tags.find(({ id }) => {
|
||||
return id === undefined;
|
||||
});
|
||||
|
||||
if (newTag && this.hasPermissionToCreateOwnTag) {
|
||||
this.adminService
|
||||
.postTag({ ...newTag, userId: this.user.id })
|
||||
.pipe(
|
||||
switchMap((createdTag) => {
|
||||
return this.dataService.putHoldingTags({
|
||||
dataSource: this.data.dataSource,
|
||||
symbol: this.data.symbol,
|
||||
tags: [
|
||||
...tags.filter(({ id }) => {
|
||||
return id !== undefined;
|
||||
}),
|
||||
createdTag
|
||||
]
|
||||
});
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe();
|
||||
} else {
|
||||
this.dataService
|
||||
.putHoldingTags({
|
||||
tags,
|
||||
dataSource: this.data.dataSource,
|
||||
symbol: this.data.symbol
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
}
|
||||
});
|
||||
|
||||
this.dataService
|
||||
@ -420,6 +448,11 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateOwnTag = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createOwnTag
|
||||
);
|
||||
|
||||
this.tagsAvailable =
|
||||
this.user?.tags?.map((tag) => {
|
||||
return {
|
||||
|
@ -388,6 +388,7 @@
|
||||
</mat-tab-group>
|
||||
|
||||
<gf-tags-selector
|
||||
[hasPermissionToCreateTag]="hasPermissionToCreateOwnTag"
|
||||
[readonly]="!data.hasPermissionToUpdateOrder"
|
||||
[tags]="activityForm.get('tags')?.value"
|
||||
[tagsAvailable]="tagsAvailable"
|
||||
|
@ -13,6 +13,7 @@ export const permissions = {
|
||||
createMarketData: 'createMarketData',
|
||||
createMarketDataOfOwnAssetProfile: 'createMarketDataOfOwnAssetProfile',
|
||||
createOrder: 'createOrder',
|
||||
createOwnTag: 'createOwnTag',
|
||||
createPlatform: 'createPlatform',
|
||||
createTag: 'createTag',
|
||||
createUserAccount: 'createUserAccount',
|
||||
@ -67,6 +68,7 @@ export function getPermissions(aRole: Role): string[] {
|
||||
permissions.createMarketData,
|
||||
permissions.createMarketDataOfOwnAssetProfile,
|
||||
permissions.createOrder,
|
||||
permissions.createOwnTag,
|
||||
permissions.createPlatform,
|
||||
permissions.createTag,
|
||||
permissions.deleteAccess,
|
||||
@ -110,6 +112,7 @@ export function getPermissions(aRole: Role): string[] {
|
||||
permissions.createAccountBalance,
|
||||
permissions.createMarketDataOfOwnAssetProfile,
|
||||
permissions.createOrder,
|
||||
permissions.createOwnTag,
|
||||
permissions.deleteAccess,
|
||||
permissions.deleteAccount,
|
||||
permissions.deleteAccountBalance,
|
||||
|
@ -43,7 +43,7 @@
|
||||
</mat-option>
|
||||
}
|
||||
|
||||
@if (hasPermissionToCreateTags && tagInputControl.value) {
|
||||
@if (hasPermissionToCreateTag && tagInputControl.value) {
|
||||
<mat-option [value]="tagInputControl.value.trim()">
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="add-circle-outline" />
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import '@angular/localize/init';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
|
||||
|
||||
@ -49,7 +50,7 @@ export const Default: Story = {
|
||||
|
||||
export const CreateCustomTags: Story = {
|
||||
args: {
|
||||
hasPermissionToCreateTags: true,
|
||||
hasPermissionToCreateTag: true,
|
||||
tags: [
|
||||
{
|
||||
id: 'EMERGENCY_FUND',
|
||||
|
@ -42,7 +42,7 @@ import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
|
||||
templateUrl: 'tags-selector.component.html'
|
||||
})
|
||||
export class GfTagsSelectorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() hasPermissionToCreateTags = false;
|
||||
@Input() hasPermissionToCreateTag = false;
|
||||
@Input() readonly = false;
|
||||
@Input() tags: Tag[];
|
||||
@Input() tagsAvailable: Tag[];
|
||||
@ -81,7 +81,7 @@ export class GfTagsSelectorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return id === event.option.value;
|
||||
});
|
||||
|
||||
if (!tag && this.hasPermissionToCreateTags) {
|
||||
if (!tag && this.hasPermissionToCreateTag) {
|
||||
tag = {
|
||||
id: undefined,
|
||||
name: event.option.value as string,
|
||||
|
66
package-lock.json
generated
66
package-lock.json
generated
@ -125,7 +125,7 @@
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/angular": "8.4.7",
|
||||
"@storybook/core-server": "8.4.7",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
||||
"@types/big.js": "6.2.2",
|
||||
"@types/cache-manager": "4.0.6",
|
||||
"@types/color": "4.2.0",
|
||||
@ -2449,13 +2449,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
|
||||
"integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz",
|
||||
"integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.26.3",
|
||||
"@babel/types": "^7.26.3",
|
||||
"@babel/parser": "^7.26.8",
|
||||
"@babel/types": "^7.26.8",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
@ -2766,12 +2766,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
|
||||
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
|
||||
"integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.3"
|
||||
"@babel/types": "^7.26.8"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@ -4149,30 +4149,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz",
|
||||
"integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.25.9",
|
||||
"@babel/parser": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.26.8",
|
||||
"@babel/types": "^7.26.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.26.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
|
||||
"integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz",
|
||||
"integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.26.3",
|
||||
"@babel/parser": "^7.26.3",
|
||||
"@babel/template": "^7.25.9",
|
||||
"@babel/types": "^7.26.3",
|
||||
"@babel/generator": "^7.26.8",
|
||||
"@babel/parser": "^7.26.8",
|
||||
"@babel/template": "^7.26.8",
|
||||
"@babel/types": "^7.26.8",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
@ -4181,9 +4181,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
|
||||
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
|
||||
"integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
@ -10472,16 +10472,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.1.tgz",
|
||||
"integrity": "sha512-NDZndt0fmVThIx/8cExuJHLZagUVzfGCoVrwH9x6aZvwfBdkrDFTYujecek6X2WpG4uUFsVaPg5+aNQPSyjcmw==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz",
|
||||
"integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.26.2",
|
||||
"@babel/parser": "^7.26.2",
|
||||
"@babel/traverse": "^7.25.9",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@babel/generator": "^7.26.5",
|
||||
"@babel/parser": "^7.26.7",
|
||||
"@babel/traverse": "^7.26.7",
|
||||
"@babel/types": "^7.26.7",
|
||||
"javascript-natural-sort": "^0.7.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
|
@ -171,7 +171,7 @@
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/angular": "8.4.7",
|
||||
"@storybook/core-server": "8.4.7",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
||||
"@types/big.js": "6.2.2",
|
||||
"@types/cache-manager": "4.0.6",
|
||||
"@types/color": "4.2.0",
|
||||
|
Loading…
x
Reference in New Issue
Block a user