Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular - Refactoring the Extension System for Using the Injection Cycle #21197

Draft
wants to merge 3 commits into
base: rel-9.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions npm/ng-packs/packages/account/src/lib/guards/extensions.guard.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable, inject } from '@angular/core';
import { Injectable, Injector, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { tap, map } from 'rxjs/operators';
import { ConfigStateService, IAbpGuard, PermissionService } from '@abp/ng.core';
import { IAbpGuard } from '@abp/ng.core';
import {
ExtensionsService,
getObjectExtensionEntitiesFromStore,
Expand All @@ -20,20 +20,19 @@ import { eAccountComponents } from '../enums/components';
*/
@Injectable()
export class AccountExtensionsGuard implements IAbpGuard {
protected readonly configState = inject(ConfigStateService);
protected readonly permmission = inject(PermissionService);
protected readonly injector = inject(Injector);
protected readonly extensions = inject(ExtensionsService);

canActivate(): Observable<boolean> {
const config = { optional: true };

const editFormContributors = inject(ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS, config) || {};

return getObjectExtensionEntitiesFromStore(this.configState, 'Identity').pipe(
return getObjectExtensionEntitiesFromStore(this.injector, 'Identity').pipe(
map(entities => ({
[eAccountComponents.PersonalSettings]: entities.User,
})),
mapEntitiesToContributors(this.configState, this.permmission, 'AbpIdentity'),
mapEntitiesToContributors(this.injector, 'AbpIdentity'),
tap(objectExtensionContributors => {
mergeWithDefaultProps(
this.extensions.editFormProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { inject } from '@angular/core';
import { inject, Injector } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { map, tap } from 'rxjs';
import { ConfigStateService, PermissionService } from '@abp/ng.core';
import {
ExtensionsService,
getObjectExtensionEntitiesFromStore,
Expand All @@ -12,19 +11,18 @@ import { eAccountComponents } from '../enums';
import { ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS, DEFAULT_ACCOUNT_FORM_PROPS } from '../tokens';

export const accountExtensionsResolver: ResolveFn<any> = () => {
const configState = inject(ConfigStateService);
const permission = inject(PermissionService);
const injector = inject(Injector);
const extensions = inject(ExtensionsService);

const config = { optional: true };

const editFormContributors = inject(ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS, config) || {};

return getObjectExtensionEntitiesFromStore(configState, 'Identity').pipe(
return getObjectExtensionEntitiesFromStore(injector, 'Identity').pipe(
map(entities => ({
[eAccountComponents.PersonalSettings]: entities.User,
})),
mapEntitiesToContributors(configState, permission, 'AbpIdentity'),
mapEntitiesToContributors(injector, 'AbpIdentity'),
tap(objectExtensionContributors => {
mergeWithDefaultProps(
extensions.editFormProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ConfigStateService, PermissionService } from '@abp/ng.core';
import { Injector } from '@angular/core';
import { Observable, of } from 'rxjs';
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties';
import {
Expand Down Expand Up @@ -52,20 +53,21 @@ export function mergeWithDefaultProps<F extends PropsFactory<any>>(
}

export function checkPolicies(
injector: Injector,
properties: ObjectExtensions.EntityExtensionProperties,
configState: ConfigStateService,
permissionService: PermissionService,
) {
const configState = injector.get(ConfigStateService);
const permission = injector.get(PermissionService);
const props = Object.entries(properties);

const checkPolicy = (policy: Policy): boolean => {
const { permissions, globalFeatures, features } = policy;
const { permissions, globalFeatures, features } = policy || {};

const checks = [
{
items: permissions?.permissionNames,
requiresAll: permissions?.requiresAll,
check: (item: string) => permissionService.getGrantedPolicy(item),
check: (item: string) => permission.getGrantedPolicy(item),
},
{
items: globalFeatures?.features,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
ExtensionEnumDto,
ExtensionPropertyUiLookupDto,
ObjectExtensionsDto,
PermissionService,
} from '@abp/ng.core';
import { Injector } from '@angular/core';
import { Observable, pipe, zip } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { ePropType } from '../enums/props.enum';
Expand Down Expand Up @@ -56,10 +56,8 @@ function selectEnums(
);
}

export function getObjectExtensionEntitiesFromStore(
configState: ConfigStateService,
moduleKey: string,
) {
export function getObjectExtensionEntitiesFromStore(injector: Injector, moduleKey: string) {
const configState = injector.get(ConfigStateService);
return selectObjectExtensions(configState).pipe(
map(extensions => {
if (!extensions) return null;
Expand All @@ -73,11 +71,8 @@ export function getObjectExtensionEntitiesFromStore(
);
}

export function mapEntitiesToContributors<T = any>(
configState: ConfigStateService,
permissionService: PermissionService,
resource: string,
) {
export function mapEntitiesToContributors<T = any>(injector: Injector, resource: string) {
const configState = injector.get(ConfigStateService);
return pipe(
switchMap((entities: any) =>
zip(selectLocalization(configState), selectEnums(configState)).pipe(
Expand All @@ -100,7 +95,7 @@ export function mapEntitiesToContributors<T = any>(
return acc;
}

checkPolicies(properties, configState, permissionService);
checkPolicies(injector, properties);

const mapPropertiesToContributors = createPropertiesToContributorsMapper<T>(
generateDisplayName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('Form Prop Utils', () => {
extraProperties: {
bool: true,
date: '03/30/2002',
dateTime: '2002-03-30 13:30:59Z',
dateTime: '2002-03-30 13:30:59',
time: '13:30:59',
},
});
Expand All @@ -117,8 +117,8 @@ describe('Form Prop Utils', () => {
expect(extraPropertiesGroup.value).toEqual({
bool: true,
date: '2002-03-30',
dateTime: '2002-03-30T13:30:59.000Z',
time: '13:30',
dateTime: '2002-03-30T13:30:59.000',
time: '13:30:59',
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { ConfigStateService, PermissionService } from '@abp/ng.core';
import { Injector } from '@angular/core';
import {
checkPolicies,
createExtraPropertyValueResolver,
mergeWithDefaultProps,
} from '../lib/utils/props.util';
import { ObjectExtensions } from '../lib/models/object-extensions';
import {
EntityProp,
EntityPropContributorCallbacks,
EntityPropDefaults,
EntityPropsFactory,
} from '../lib/models/entity-props';
import { PropData } from '../lib/models/props';
import { createExtraPropertyValueResolver, mergeWithDefaultProps } from '../lib/utils/props.util';

class MockPropData<R = any> extends PropData<R> {
getInjected: PropData<R>['getInjected'];
Expand Down Expand Up @@ -69,4 +76,194 @@ describe('Entity Prop Utils', () => {
expect(entityProps.get('y').props.toString()).toBe('3 <-> 2');
});
});

describe('#checkPolicies', () => {
let injector: Injector;
let configState: ConfigStateService;
let permissionService: PermissionService;

beforeEach(() => {
jest.clearAllMocks();

configState = {
getGlobalFeatureIsEnabled: jest.fn().mockReturnValue(false),
getFeatureIsEnabled: jest.fn().mockReturnValue(false),
} as any;

permissionService = {
getGrantedPolicy: jest.fn().mockReturnValue(false),
} as any;

injector = {
get: jest.fn().mockImplementation(service => {
if (service === ConfigStateService) {
return configState;
}
if (service === PermissionService) {
return permissionService;
}
return null;
}),
} as any;
});

it('should keep property when all permissions and features are satisfied', () => {
(permissionService.getGrantedPolicy as jest.Mock).mockReturnValue(true);
(configState.getGlobalFeatureIsEnabled as jest.Mock).mockReturnValue(true);
(configState.getFeatureIsEnabled as jest.Mock).mockReturnValue(true);

const properties: ObjectExtensions.EntityExtensionProperties = {
property1: {
policy: {
permissions: { permissionNames: ['Permission1', 'Permission2'], requiresAll: true },
globalFeatures: { features: ['GlobalFeature1'], requiresAll: true },
features: { features: ['Feature1'], requiresAll: true },
},
displayName: undefined,
api: undefined,
ui: undefined,
attributes: [],
configuration: undefined,
defaultValue: undefined,
},
};

checkPolicies(injector, properties);

expect(properties.property1).toBeDefined();
expect(permissionService.getGrantedPolicy).toHaveBeenCalledWith('Permission1');
expect(permissionService.getGrantedPolicy).toHaveBeenCalledWith('Permission2');
expect(configState.getGlobalFeatureIsEnabled).toHaveBeenCalledWith('GlobalFeature1');
expect(configState.getFeatureIsEnabled).toHaveBeenCalledWith('Feature1');
});

it('should keep property when no permissions and features are satisfied', () => {
(permissionService.getGrantedPolicy as jest.Mock).mockReturnValue(false);
(configState.getGlobalFeatureIsEnabled as jest.Mock).mockReturnValue(false);
(configState.getFeatureIsEnabled as jest.Mock).mockReturnValue(false);

const properties: ObjectExtensions.EntityExtensionProperties = {
property1: {
policy: {
permissions: { permissionNames: ['Permission1', 'Permission2'], requiresAll: true },
globalFeatures: { features: ['GlobalFeature1'], requiresAll: true },
features: { features: ['Feature1'], requiresAll: true },
},
displayName: undefined,
api: undefined,
ui: undefined,
attributes: [],
configuration: undefined,
defaultValue: undefined,
},
};

checkPolicies(injector, properties);

expect(properties.property1).toBeUndefined();
expect(permissionService.getGrantedPolicy).toHaveBeenCalledWith('Permission1');
expect(permissionService.getGrantedPolicy).not.toHaveBeenCalledWith('Permission2');
expect(configState.getGlobalFeatureIsEnabled).not.toHaveBeenCalledWith('GlobalFeature1');
expect(configState.getFeatureIsEnabled).not.toHaveBeenCalledWith('Feature1');
});

it('should delete property when only some permissions are granted', () => {
(permissionService.getGrantedPolicy as jest.Mock).mockImplementation(
permission => permission === 'Permission1',
);
(configState.getGlobalFeatureIsEnabled as jest.Mock).mockReturnValue(true);
(configState.getFeatureIsEnabled as jest.Mock).mockReturnValue(true);

const properties: ObjectExtensions.EntityExtensionProperties = {
property1: {
policy: {
permissions: { permissionNames: ['Permission1', 'Permission2'], requiresAll: true },
globalFeatures: { features: ['GlobalFeature1'], requiresAll: true },
features: { features: ['Feature1'], requiresAll: true },
},
displayName: undefined,
api: undefined,
ui: undefined,
attributes: [],
configuration: undefined,
defaultValue: undefined,
},
};

checkPolicies(injector, properties);

expect(properties.property1).toBeUndefined();
expect(permissionService.getGrantedPolicy).toHaveBeenCalledWith('Permission1');
expect(permissionService.getGrantedPolicy).toHaveBeenCalledWith('Permission2');
});

it('should delete property when some global features are disabled', () => {
(permissionService.getGrantedPolicy as jest.Mock).mockReturnValue(true);
(configState.getGlobalFeatureIsEnabled as jest.Mock).mockImplementation(feature =>
feature === 'GlobalFeature2' ? false : true,
);
(configState.getFeatureIsEnabled as jest.Mock).mockReturnValue(true);

const properties: ObjectExtensions.EntityExtensionProperties = {
property1: {
policy: {
permissions: { permissionNames: ['Permission1'], requiresAll: true },
globalFeatures: { features: ['GlobalFeature1', 'GlobalFeature2'], requiresAll: true },
features: { features: ['Feature1'], requiresAll: true },
},
displayName: undefined,
api: undefined,
ui: undefined,
attributes: [],
configuration: undefined,
defaultValue: undefined,
},
};

checkPolicies(injector, properties);

expect(properties.property1).toBeUndefined();
expect(configState.getGlobalFeatureIsEnabled).toHaveBeenCalledWith('GlobalFeature1');
expect(configState.getGlobalFeatureIsEnabled).toHaveBeenCalledWith('GlobalFeature2');
});

it('should keep property when all permissions are granted but only some features are required', () => {
(permissionService.getGrantedPolicy as jest.Mock).mockImplementation(
permission => permission === 'Permission1' || permission === 'Permission2',
);
(configState.getFeatureIsEnabled as jest.Mock).mockImplementation(
feature => feature === 'Feature1',
);
(configState.getGlobalFeatureIsEnabled as jest.Mock).mockReturnValue(true);

const properties: ObjectExtensions.EntityExtensionProperties = {
property1: {
policy: {
permissions: { permissionNames: ['Permission1', 'Permission2'], requiresAll: false },
features: {
features: ['Feature1', 'Feature2', 'Feature3'],
requiresAll: false,
},
globalFeatures: { features: ['GlobalFeature1'], requiresAll: true },
},
displayName: undefined,
api: undefined,
ui: undefined,
attributes: [],
configuration: undefined,
defaultValue: undefined,
},
};

checkPolicies(injector, properties);

expect(properties.property1).toBeDefined();

expect(configState.getFeatureIsEnabled).toHaveBeenCalledWith('Feature1');
expect(configState.getFeatureIsEnabled).not.toHaveBeenCalledWith('Feature2');
expect(configState.getFeatureIsEnabled).not.toHaveBeenCalledWith('Feature3');

expect(configState.getGlobalFeatureIsEnabled).toHaveBeenCalledWith('GlobalFeature1');
});
});
});
Loading
Loading