Browse Source

Add write access

pull/7676/head
Thomas Kaul 1 week ago
parent
commit
a22fb33e7f
  1. 9
      apps/client/src/app/components/access-table/access-table.component.html
  2. 6
      apps/client/src/app/components/access-table/access-table.component.ts
  3. 4
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  4. 1
      apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts
  5. 4
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  6. 9
      apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
  7. 23
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  8. 13
      apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts
  9. 13
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  10. 6
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  11. 10
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  12. 13
      apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts
  13. 4
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  14. 17
      libs/common/src/lib/scopes.spec.ts
  15. 6
      libs/common/src/lib/scopes.ts
  16. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  17. 16
      libs/ui/src/lib/activities-table/activities-table.component.html
  18. 2
      libs/ui/src/lib/activities-table/activities-table.component.ts

9
apps/client/src/app/components/access-table/access-table.component.html

@ -18,17 +18,16 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> <td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
@if (hasScopeToReadValues(element)) { @if (hasScopesToWrite(element)) {
<ion-icon class="mr-1" name="create-outline" />
<ng-container i18n>View and manage</ng-container>
} @else if (hasScopeToReadValues(element)) {
<ion-icon class="mr-1" name="lock-open-outline" /> <ion-icon class="mr-1" name="lock-open-outline" />
<ng-container i18n>View</ng-container> <ng-container i18n>View</ng-container>
} @else { } @else {
<ion-icon class="mr-1" name="lock-closed-outline" /> <ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted view</ng-container> <ng-container i18n>Restricted view</ng-container>
} }
@if (hasScopesToWrite(element)) {
<ion-icon class="ml-2 mr-1" name="create-outline" />
<ng-container i18n>Changes</ng-container>
}
</div> </div>
</td> </td>
</ng-container> </ng-container>

6
apps/client/src/app/components/access-table/access-table.component.ts

@ -2,7 +2,7 @@ import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { import {
SCOPES_OF_WRITE_ACCESS, hasAnyScopeOfWriteAccess,
hasScope, hasScope,
scopes scopes
} from '@ghostfolio/common/scopes'; } from '@ghostfolio/common/scopes';
@ -110,9 +110,7 @@ export class GfAccessTableComponent {
} }
protected hasScopesToWrite({ scopes: scopesOfAccess }: Access) { protected hasScopesToWrite({ scopes: scopesOfAccess }: Access) {
return SCOPES_OF_WRITE_ACCESS.some((scope) => { return hasAnyScopeOfWriteAccess(scopesOfAccess);
return hasScope(scopesOfAccess, scope);
});
} }
protected onCopyUrlToClipboard(aId: string) { protected onCopyUrlToClipboard(aId: string) {

4
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -148,11 +148,13 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasActivities]="true"
[hasPermissionToCreateActivity]="data.hasPermissionToCreateActivity"
[hasPermissionToDeleteActivity]="false" [hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]="!showValuesInPercentage()" [hasPermissionToExportActivities]="!showValuesInPercentage()"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[hasPermissionToUpdateActivity]="data.hasPermissionToUpdateActivity"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[pageIndex]="pageIndex" [pageIndex]="pageIndex"
[pageSize]="pageSize" [pageSize]="pageSize"

1
apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts

@ -2,6 +2,7 @@ export interface AccountDetailDialogParams {
accountId: string; accountId: string;
deviceType: string; deviceType: string;
hasPermissionToCreateActivity: boolean; hasPermissionToCreateActivity: boolean;
hasPermissionToUpdateActivity: boolean;
impersonationId: string | null; impersonationId: string | null;
} }

4
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -379,13 +379,15 @@
[baseCurrency]="data.baseCurrency" [baseCurrency]="data.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasActivities]="true"
[hasPermissionToCreateActivity]="data.hasPermissionToCreateActivity"
[hasPermissionToDeleteActivity]="false" [hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]=" [hasPermissionToExportActivities]="
!data.impersonationId && !user?.settings?.isRestrictedView !data.impersonationId && !user?.settings?.isRestrictedView
" "
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[hasPermissionToUpdateActivity]="data.hasPermissionToUpdateActivity"
[locale]="data.locale" [locale]="data.locale"
[pageIndex]="pageIndex" [pageIndex]="pageIndex"
[pageSize]="pageSize" [pageSize]="pageSize"

9
apps/client/src/app/components/home-watchlist/home-watchlist.component.ts

@ -136,6 +136,15 @@ export class GfHomeWatchlistComponent implements OnInit {
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
if (
!hasPermission(user?.permissions, permissions.createWatchlistItem) ||
!hasScope(user?.scopes, scopes.watchlistCreate)
) {
this.router.navigate(['.'], { relativeTo: this.route });
return;
}
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfCreateWatchlistItemDialogComponent, GfCreateWatchlistItemDialogComponent,
CreateWatchlistItemDialogParams CreateWatchlistItemDialogParams

23
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -6,6 +6,7 @@ import {
SCOPES_OF_READ_RESTRICTED_ACCESS, SCOPES_OF_READ_RESTRICTED_ACCESS,
SCOPES_OF_WRITE_ACCESS, SCOPES_OF_WRITE_ACCESS,
Scope, Scope,
hasAnyScopeOfWriteAccess,
hasScope, hasScope,
scopes scopes
} from '@ghostfolio/common/scopes'; } from '@ghostfolio/common/scopes';
@ -186,7 +187,21 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
private buildScopes(): Scope[] { private buildScopes(): Scope[] {
switch (this.accessForm.get('accessLevel')?.value as AccessLevel) { const accessLevel = this.accessForm.get('accessLevel')
?.value as AccessLevel;
const scopesOfAccess = this.data.access?.scopes ?? [];
if (
scopesOfAccess.length > 0 &&
accessLevel === this.getAccessLevel(scopesOfAccess)
) {
return Object.values(scopes).filter((scope) => {
return hasScope(scopesOfAccess, scope);
});
}
switch (accessLevel) {
case 'CREATE_READ_UPDATE_DELETE': case 'CREATE_READ_UPDATE_DELETE':
return [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS]; return [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS];
case 'READ': case 'READ':
@ -236,11 +251,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
private getAccessLevel(scopesOfAccess: string[] | undefined): AccessLevel { private getAccessLevel(scopesOfAccess: string[] | undefined): AccessLevel {
if ( if (hasAnyScopeOfWriteAccess(scopesOfAccess)) {
SCOPES_OF_WRITE_ACCESS.some((scope) => {
return hasScope(scopesOfAccess, scope);
})
) {
return 'CREATE_READ_UPDATE_DELETE'; return 'CREATE_READ_UPDATE_DELETE';
} }

13
apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts

@ -112,7 +112,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
if ( if (
!account || !account ||
!hasPermission(user?.permissions, permissions.updateAccount) || !hasPermission(user?.permissions, permissions.updateAccount) ||
this.isReadOnlyMode(user, scopes.accountUpdate) this.isWriteRestricted(user, scopes.accountUpdate)
) { ) {
this.navigateBack(); this.navigateBack();
@ -141,7 +141,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
if ( if (
!hasPermission(user?.permissions, permissions.createAccount) || !hasPermission(user?.permissions, permissions.createAccount) ||
this.isReadOnlyMode(user, scopes.accountCreate) this.isWriteRestricted(user, scopes.accountCreate)
) { ) {
this.navigateBack(); this.navigateBack();
@ -182,10 +182,9 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
this.dialogRef?.close(); this.dialogRef?.close();
} }
private isReadOnlyMode(user: User, requiredScope: Scope) { private isWriteRestricted(user: User, requiredScope: Scope) {
return ( return (
(!!this.impersonationStorageService.getId() && !hasScope(user?.scopes, requiredScope) ||
!hasScope(user?.scopes, requiredScope)) ||
!!user?.settings?.isRestrictedView !!user?.settings?.isRestrictedView
); );
} }
@ -222,6 +221,10 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
hasPermissionToCreateActivity: hasPermissionToCreateActivity:
!impersonationId && !impersonationId &&
hasPermission(user?.permissions, permissions.createActivity) && hasPermission(user?.permissions, permissions.createActivity) &&
!user?.settings?.isRestrictedView,
hasPermissionToUpdateActivity:
!impersonationId &&
hasPermission(user?.permissions, permissions.updateActivity) &&
!user?.settings?.isRestrictedView !user?.settings?.isRestrictedView
}, },
height: this.deviceType() === 'mobile' ? '98vh' : '80vh', height: this.deviceType() === 'mobile' ? '98vh' : '80vh',

13
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -1,3 +1,4 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TransferBalanceDto } from '@ghostfolio/common/dtos'; import { TransferBalanceDto } from '@ghostfolio/common/dtos';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
@ -48,6 +49,7 @@ export class GfAccountsPageComponent implements OnInit {
protected totalValueInBaseCurrency = 0; protected totalValueInBaseCurrency = 0;
protected user: User; protected user: User;
private hasImpersonationId: boolean;
private isInitialFetch = true; private isInitialFetch = true;
private readonly deviceType = computed( private readonly deviceType = computed(
@ -59,6 +61,9 @@ export class GfAccountsPageComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog); private readonly dialog = inject(MatDialog);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly notificationService = inject(NotificationService); private readonly notificationService = inject(NotificationService);
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
@ -75,6 +80,13 @@ export class GfAccountsPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
@ -137,6 +149,7 @@ export class GfAccountsPageComponent implements OnInit {
this.totalValueInBaseCurrency = totalValueInBaseCurrency; this.totalValueInBaseCurrency = totalValueInBaseCurrency;
if ( if (
!this.hasImpersonationId &&
this.accounts?.length <= 0 && this.accounts?.length <= 0 &&
this.hasPermissionToCreateAccount && this.hasPermissionToCreateAccount &&
this.isInitialFetch this.isInitialFetch

6
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -77,6 +77,12 @@ export class GfActivitiesPageComponent implements OnInit {
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
protected get hasPermissionToImportActivities() {
// An import always writes to the own portfolio, hence it is not available
// while the user impersonates a different user
return this.hasPermissionToCreateActivity && !this.hasImpersonationId;
}
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;

10
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -7,15 +7,11 @@
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="deviceType" [deviceType]="deviceType"
[hasActivities]="user?.activitiesCount > 0" [hasActivities]="user?.activitiesCount > 0"
[hasPermissionToCreateActivity]=" [hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
hasPermissionToCreateActivity && !hasImpersonationId
"
[hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity" [hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity"
[hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToExportActivities]="!hasImpersonationId"
[hasPermissionToFilterByType]="user?.settings?.isExperimentalFeatures" [hasPermissionToFilterByType]="user?.settings?.isExperimentalFeatures"
[hasPermissionToImportActivities]=" [hasPermissionToImportActivities]="hasPermissionToImportActivities"
hasPermissionToCreateActivity && !hasImpersonationId
"
[hasPermissionToUpdateActivity]="hasPermissionToUpdateActivity" [hasPermissionToUpdateActivity]="hasPermissionToUpdateActivity"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[pageIndex]="pageIndex" [pageIndex]="pageIndex"
@ -43,7 +39,7 @@
</div> </div>
</div> </div>
@if (hasPermissionToCreateActivity && !user.settings.isRestrictedView) { @if (hasPermissionToCreateActivity) {
<gf-fab <gf-fab
[routerLink]=" [routerLink]="
internalRoutes.portfolio.subRoutes.activities.subRoutes.create internalRoutes.portfolio.subRoutes.activities.subRoutes.create

13
apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts

@ -1,4 +1,3 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos';
import { Activity, User } from '@ghostfolio/common/interfaces'; import { Activity, User } from '@ghostfolio/common/interfaces';
@ -47,9 +46,6 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog); private readonly dialog = inject(MatDialog);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
@ -95,7 +91,7 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit {
if ( if (
!activity || !activity ||
!hasPermission(user?.permissions, permissions.updateActivity) || !hasPermission(user?.permissions, permissions.updateActivity) ||
this.isReadOnlyMode(user, scopes.activityUpdate) this.isWriteRestricted(user, scopes.activityUpdate)
) { ) {
this.navigateBack(); this.navigateBack();
@ -116,7 +112,7 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit {
// Cloning creates a new activity as well // Cloning creates a new activity as well
if ( if (
!hasPermission(user?.permissions, permissions.createActivity) || !hasPermission(user?.permissions, permissions.createActivity) ||
this.isReadOnlyMode(user, scopes.activityCreate) this.isWriteRestricted(user, scopes.activityCreate)
) { ) {
this.navigateBack(); this.navigateBack();
@ -157,10 +153,9 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit {
this.dialogRef?.close(); this.dialogRef?.close();
} }
private isReadOnlyMode(user: User, requiredScope: Scope) { private isWriteRestricted(user: User, requiredScope: Scope) {
return ( return (
(!!this.impersonationStorageService.getId() && !hasScope(user?.scopes, requiredScope) ||
!hasScope(user?.scopes, requiredScope)) ||
!!user?.settings?.isRestrictedView !!user?.settings?.isRestrictedView
); );
} }

4
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -626,6 +626,10 @@ export class GfAllocationsPageComponent implements OnInit {
!this.impersonationId && !this.impersonationId &&
hasPermission(this.user?.permissions, permissions.createActivity) && hasPermission(this.user?.permissions, permissions.createActivity) &&
!this.user?.settings?.isRestrictedView, !this.user?.settings?.isRestrictedView,
hasPermissionToUpdateActivity:
!this.impersonationId &&
hasPermission(this.user?.permissions, permissions.updateActivity) &&
!this.user?.settings?.isRestrictedView,
impersonationId: this.impersonationId impersonationId: this.impersonationId
}, },
height: this.deviceType() === 'mobile' ? '98vh' : '80vh', height: this.deviceType() === 'mobile' ? '98vh' : '80vh',

17
libs/common/src/lib/scopes.spec.ts

@ -5,6 +5,7 @@ import {
getScopesOfAccess, getScopesOfAccess,
getScopesOfOwnAccess, getScopesOfOwnAccess,
getScopesOfUnrestrictedImpersonation, getScopesOfUnrestrictedImpersonation,
hasAnyScopeOfWriteAccess,
hasScope, hasScope,
scopes scopes
} from '@ghostfolio/common/scopes'; } from '@ghostfolio/common/scopes';
@ -189,6 +190,22 @@ describe('Scopes', () => {
}); });
}); });
describe('Has any scope of write access', () => {
it('Single write scope', () => {
expect(hasAnyScopeOfWriteAccess([scopes.activityUpdate])).toEqual(true);
});
it('Read scopes only', () => {
expect(hasAnyScopeOfWriteAccess([...SCOPES_OF_READ_ACCESS])).toEqual(
false
);
});
it('Without scopes', () => {
expect(hasAnyScopeOfWriteAccess(undefined)).toEqual(false);
});
});
describe('Has scope', () => { describe('Has scope', () => {
it('Present scope', () => { it('Present scope', () => {
expect(hasScope([scopes.portfolioRead], scopes.portfolioRead)).toEqual( expect(hasScope([scopes.portfolioRead], scopes.portfolioRead)).toEqual(

6
libs/common/src/lib/scopes.ts

@ -97,6 +97,12 @@ export function getScopesOfUnrestrictedImpersonation(): string[] {
return [...SCOPES_OF_READ_RESTRICTED_ACCESS]; return [...SCOPES_OF_READ_RESTRICTED_ACCESS];
} }
export function hasAnyScopeOfWriteAccess(aScopes: string[] = []) {
return SCOPES_OF_WRITE_ACCESS.some((scope) => {
return hasScope(aScopes, scope);
});
}
export function hasScope(aScopes: string[] = [], aScope: Scope) { export function hasScope(aScopes: string[] = [], aScope: Scope) {
return aScopes.includes(aScope); return aScopes.includes(aScope);
} }

2
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -1,4 +1,4 @@
@if (showActions()) { @if (showActions() && hasPermissionToUpdateAccount()) {
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"

16
libs/ui/src/lib/activities-table/activities-table.component.html

@ -19,8 +19,9 @@
} }
</div> </div>
@if (hasPermissionToImportActivities) { @if (hasPermissionToImportActivities || hasPermissionToDeleteActivity) {
<div class="d-flex"> <div class="d-flex">
@if (hasPermissionToImportActivities) {
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
mat-stroked-button mat-stroked-button
@ -29,7 +30,8 @@
<ion-icon class="mr-2" name="cloud-upload-outline" /> <ion-icon class="mr-2" name="cloud-upload-outline" />
<span><ng-container i18n>Import Activities</ng-container>...</span> <span><ng-container i18n>Import Activities</ng-container>...</span>
</button> </button>
@if (hasPermissionToExportActivities) { }
@if (hasPermissionToExportActivities || hasPermissionToDeleteActivity) {
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-stroked-button mat-stroked-button
@ -44,6 +46,7 @@
class="no-max-width" class="no-max-width"
xPosition="before" xPosition="before"
> >
@if (hasPermissionToImportActivities) {
<button <button
mat-menu-item mat-menu-item
[disabled]="dataSource()?.data.length === 0" [disabled]="dataSource()?.data.length === 0"
@ -54,6 +57,7 @@
<span><ng-container i18n>Import Dividends</ng-container>...</span> <span><ng-container i18n>Import Dividends</ng-container>...</span>
</span> </span>
</button> </button>
}
@if (hasPermissionToExportActivities) { @if (hasPermissionToExportActivities) {
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
@ -87,7 +91,12 @@
</span> </span>
</button> </button>
} }
@if (hasPermissionToDeleteActivity) {
@if (
hasPermissionToExportActivities || hasPermissionToImportActivities
) {
<hr class="m-0" /> <hr class="m-0" />
}
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
mat-menu-item mat-menu-item
@ -106,6 +115,7 @@
} }
</span> </span>
</button> </button>
}
</mat-menu> </mat-menu>
</div> </div>
} }
@ -504,6 +514,7 @@
</span> </span>
</a> </a>
} }
@if (hasPermissionToCreateActivity) {
<a <a
mat-menu-item mat-menu-item
[routerLink]="activityDialogRouterLinks().get(element.id)?.clone" [routerLink]="activityDialogRouterLinks().get(element.id)?.clone"
@ -513,6 +524,7 @@
<span><ng-container i18n>Clone</ng-container>...</span> <span><ng-container i18n>Clone</ng-container>...</span>
</span> </span>
</a> </a>
}
<button <button
mat-menu-item mat-menu-item
[disabled]="!isDraftActivity(element)" [disabled]="!isDraftActivity(element)"

2
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -116,7 +116,7 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
@Input() hasPermissionToFilterByType: boolean; @Input() hasPermissionToFilterByType: boolean;
@Input() hasPermissionToImportActivities: boolean; @Input() hasPermissionToImportActivities: boolean;
@Input() hasPermissionToOpenDetails = true; @Input() hasPermissionToOpenDetails = true;
@Input() hasPermissionToUpdateActivity = true; @Input() hasPermissionToUpdateActivity: boolean;
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() pageIndex: number; @Input() pageIndex: number;
@Input() pageSize = DEFAULT_PAGE_SIZE; @Input() pageSize = DEFAULT_PAGE_SIZE;

Loading…
Cancel
Save