Browse Source

Refactor components to use Angular signal inputs/outputs

Migrates Tree, FeatureManagement, and PermissionManagement components to use Angular's new signal-based input and output APIs. Updates templates and internal logic to access input values as functions, replaces EventEmitter with output signals, and synchronizes internal state using signals and effects. Also updates related model interfaces to use InputSignal and OutputEmitterRef types.
pull/24777/head
Fahri Gedik 8 months ago
parent
commit
0c09657851
  1. 8
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html
  2. 121
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts
  3. 6
      npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html
  4. 62
      npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts
  5. 9
      npm/ng-packs/packages/feature-management/src/lib/models/feature-management.ts
  6. 6
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html
  7. 130
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts
  8. 11
      npm/ng-packs/packages/permission-management/src/lib/models/permission-management.ts

8
npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html

@ -1,8 +1,8 @@
<nz-tree
[nzBeforeDrop]="beforeDrop"
[nzDraggable]="draggable"
[nzCheckStrictly]="checkStrictly"
[nzCheckable]="checkable"
[nzDraggable]="draggable()"
[nzCheckStrictly]="checkStrictly()"
[nzCheckable]="checkable()"
[nzCheckedKeys]="checkedKeys"
[nzData]="nodes"
[nzTreeTemplate]="treeTemplate"
@ -11,7 +11,7 @@
(nzExpandChange)="onExpandedKeysChange($event)"
(nzCheckboxChange)="onCheckboxChange($event)"
(nzOnDrop)="onDrop($event)"
[nzNoAnimation]="noAnimation"
[nzNoAnimation]="noAnimation()"
(nzContextMenu)="onContextMenuChange($event)"
/>
<ng-template #treeTemplate let-node>

121
npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts

@ -3,13 +3,14 @@ import {
ChangeDetectorRef,
Component,
ContentChild,
EventEmitter,
inject,
Input,
OnInit,
Output,
TemplateRef,
ViewEncapsulation,
input,
output,
signal,
effect
} from '@angular/core';
import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap';
import {
@ -56,32 +57,66 @@ export class TreeComponent implements OnInit {
private cdr = inject(ChangeDetectorRef);
private disableTreeStyleLoading = inject(DISABLE_TREE_STYLE_LOADING_TOKEN, { optional: true });
dropPosition: number;
dropPosition!: number;
dropdowns = {} as { [key: string]: NgbDropdown };
@ContentChild('menu') menu: TemplateRef<any>;
@ContentChild(TreeNodeTemplateDirective) customNodeTemplate: TreeNodeTemplateDirective;
@ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate: ExpandedIconTemplateDirective;
@Output() readonly checkedKeysChange = new EventEmitter();
@Output() readonly expandedKeysChange = new EventEmitter<string[]>();
@Output() readonly selectedNodeChange = new EventEmitter();
@Output() readonly dropOver = new EventEmitter<DropEvent>();
@Output() readonly nzExpandChange = new EventEmitter<NzFormatEmitEvent>();
@Input() noAnimation = true;
@Input() draggable: boolean;
@Input() checkable: boolean;
@Input() checkStrictly: boolean;
@Input() checkedKeys = [];
@Input() nodes = [];
@Input() expandedKeys: string[] = [];
@Input() selectedNode: any;
@Input() changeCheckboxWithNode: boolean;
@Input() isNodeSelected = node => this.selectedNode?.id === node.key;
@Input() beforeDrop = (event: NzFormatBeforeDropEvent) => {
this.dropPosition = event.pos;
return of(false);
};
@ContentChild('menu') menu!: TemplateRef<any>;
@ContentChild(TreeNodeTemplateDirective) customNodeTemplate!: TreeNodeTemplateDirective;
@ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate!: ExpandedIconTemplateDirective;
// Output signals
readonly checkedKeysChange = output<any>();
readonly expandedKeysChange = output<string[]>();
readonly selectedNodeChange = output<any>();
readonly dropOver = output<DropEvent>();
readonly nzExpandChange = output<NzFormatEmitEvent>();
// Input signals
readonly noAnimation = input(true);
readonly draggable = input<boolean | undefined>(undefined);
readonly checkable = input<boolean | undefined>(undefined);
readonly checkStrictly = input<boolean | undefined>(undefined);
readonly checkedKeysInput = input<any[]>([], { alias: 'checkedKeys' });
readonly nodesInput = input<any[]>([], { alias: 'nodes' });
readonly expandedKeysInput = input<string[]>([], { alias: 'expandedKeys' });
readonly selectedNodeInput = input<any>(undefined, { alias: 'selectedNode' });
readonly changeCheckboxWithNode = input<boolean | undefined>(undefined);
readonly isNodeSelectedFn = input<(node: any) => boolean>(
(node) => this._selectedNode() === node.key,
{ alias: 'isNodeSelected' }
);
readonly beforeDropFn = input<(event: NzFormatBeforeDropEvent) => any>(
(event: NzFormatBeforeDropEvent) => {
this.dropPosition = event.pos;
return of(false);
},
{ alias: 'beforeDrop' }
);
// Internal signals for two-way binding
protected readonly _checkedKeys = signal<any[]>([]);
protected readonly _expandedKeys = signal<string[]>([]);
protected readonly _selectedNode = signal<any>(undefined);
protected readonly _nodes = signal<any[]>([]);
// Getters for template access
get checkedKeys() { return this._checkedKeys(); }
get expandedKeys() { return this._expandedKeys(); }
get selectedNode() { return this._selectedNode(); }
get nodes() { return this._nodes(); }
get isNodeSelected() { return this.isNodeSelectedFn(); }
get beforeDrop() { return this.beforeDropFn(); }
constructor() {
// Sync input signals to internal signals
effect(() => {
this._checkedKeys.set(this.checkedKeysInput());
this._expandedKeys.set(this.expandedKeysInput());
this._selectedNode.set(this.selectedNodeInput());
this._nodes.set(this.nodesInput());
});
}
ngOnInit() {
this.loadStyle();
@ -97,13 +132,13 @@ export class TreeComponent implements OnInit {
this.subscriptionService.addOne(loaded$);
}
private findNode(target: any, nodes: any[]) {
private findNode(target: any, nodes: any[]): any {
for (const node of nodes) {
if (node.key === target.id) {
return node;
}
if (node.children) {
const res = this.findNode(target, node.children);
const res: any = this.findNode(target, node.children);
if (res) {
return res;
}
@ -113,36 +148,36 @@ export class TreeComponent implements OnInit {
}
onSelectedNodeChange(node: NzTreeNode) {
this.selectedNode = node.origin.entity;
if (this.changeCheckboxWithNode) {
this._selectedNode.set(node.origin.entity);
if (this.changeCheckboxWithNode()) {
let newVal;
if (node.isChecked) {
newVal = this.checkedKeys.filter(x => x !== node.key);
newVal = this._checkedKeys().filter(x => x !== node.key);
} else {
newVal = [...this.checkedKeys, node.key];
newVal = [...this._checkedKeys(), node.key];
}
this.selectedNodeChange.emit(node);
this.checkedKeys = newVal;
this._checkedKeys.set(newVal);
this.checkedKeysChange.emit(newVal);
} else {
this.selectedNodeChange.emit(node.origin.entity);
}
}
onCheckboxChange(event) {
this.checkedKeys = [...event.keys];
onCheckboxChange(event: { keys: any[] }) {
this._checkedKeys.set([...event.keys]);
this.checkedKeysChange.emit(event.keys);
}
onExpandedKeysChange(event) {
this.expandedKeys = [...event.keys];
onExpandedKeysChange(event: { keys: string[] } & NzFormatEmitEvent) {
this._expandedKeys.set([...event.keys]);
this.expandedKeysChange.emit(event.keys);
this.nzExpandChange.emit(event);
}
onDrop(event: DropEvent) {
event.event.stopPropagation();
event.event.preventDefault();
event.event?.stopPropagation();
event.event?.preventDefault();
event.pos = this.dropPosition;
this.dropOver.emit(event);
@ -160,12 +195,14 @@ export class TreeComponent implements OnInit {
dropdown.close();
}
});
this.dropdowns[dropdownKey]?.toggle();
if (dropdownKey) {
this.dropdowns[dropdownKey]?.toggle();
}
}
setSelectedNode(node: any) {
const newSelectedNode = this.findNode(node, this.nodes);
this.selectedNode = { ...newSelectedNode };
const newSelectedNode = this.findNode(node, this._nodes());
this._selectedNode.set({ ...newSelectedNode });
this.cdr.markForCheck();
}
}

6
npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html

@ -3,8 +3,8 @@
<ng-template #abpHeader>
<h3>
{{ 'AbpFeatureManagement::Features' | abpLocalization }}
@if (providerTitle) {
- {{ providerTitle }}
@if (providerTitle()) {
- {{ providerTitle() }}
}
</h3>
</ng-template>
@ -38,7 +38,7 @@
@for (feature of features[group.name]; track feature.id || i; let i = $index) {
@let provider = feature.provider.name;
@let isFeatureDisabled = !feature.parentName ? isParentDisabled(feature.name, group.name, provider) :
(provider !== providerName && provider !== defaultProviderName);
(provider !== providerName() && provider !== defaultProviderName);
<div class="mt-2" [style]="feature.style" (keyup.enter)="save()">
@switch (feature.valueType?.name) {

62
npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output, inject, DOCUMENT } from '@angular/core';
import { Component, inject, DOCUMENT, input, output, signal, effect } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ConfigStateService, LocalizationPipe, TrackByService } from '@abp/ng.core';
@ -61,14 +61,17 @@ export class FeatureManagementComponent
protected readonly confirmationService = inject(ConfirmationService);
private document = inject(DOCUMENT);
@Input()
providerKey: string;
// Signal inputs
readonly providerKey = input<string | undefined>(undefined);
readonly providerName = input<string | undefined>(undefined);
readonly providerTitle = input<string | undefined>(undefined);
readonly visibleInput = input(false, { alias: 'visible' });
@Input()
providerName: string;
// Output signals
readonly visibleChange = output<boolean>();
@Input({ required: false })
providerTitle: string;
// Internal state
protected readonly _visible = signal(false);
selectedGroupDisplayName: string;
@ -82,33 +85,41 @@ export class FeatureManagementComponent
defaultProviderName = DEFAULT_PROVIDER_NAME;
protected _visible;
modalBusy = false;
@Input()
// Getter/setter for backward compatibility
get visible(): boolean {
return this._visible;
return this._visible();
}
set visible(value: boolean) {
if (this._visible === value) {
if (this._visible() === value) {
return;
}
this._visible = value;
this._visible.set(value);
this.visibleChange.emit(value);
if (value) {
this.openModal();
return;
}
}
@Output() readonly visibleChange = new EventEmitter<boolean>();
modalBusy = false;
constructor() {
// Sync visible input to internal signal
effect(() => {
const inputValue = this.visibleInput();
if (this._visible() !== inputValue) {
this._visible.set(inputValue);
if (inputValue) {
this.openModal();
}
}
});
}
openModal() {
if (!this.providerName) {
if (!this.providerName()) {
throw new Error('providerName is required.');
}
@ -116,7 +127,7 @@ export class FeatureManagementComponent
}
getFeatures() {
this.service.get(this.providerName, this.providerKey).subscribe(res => {
this.service.get(this.providerName()!, this.providerKey()).subscribe(res => {
if (!res.groups?.length) return;
this.groups = res.groups.map(({ name, displayName }) => ({ name, displayName }));
this.selectedGroupDisplayName = this.groups[0].displayName;
@ -149,13 +160,13 @@ export class FeatureManagementComponent
this.modalBusy = true;
this.service
.update(this.providerName, this.providerKey, { features: changedFeatures })
.update(this.providerName()!, this.providerKey(), { features: changedFeatures })
.pipe(finalize(() => (this.modalBusy = false)))
.subscribe(() => {
this.visible = false;
this.toasterService.success('AbpUi::SavedSuccessfully');
if (!this.providerKey) {
if (!this.providerKey()) {
// to refresh host's features
this.configState.refreshAppState().subscribe();
}
@ -167,11 +178,11 @@ export class FeatureManagementComponent
.warn('AbpFeatureManagement::AreYouSureToResetToDefault', 'AbpFeatureManagement::AreYouSure')
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.service.delete(this.providerName, this.providerKey).subscribe(() => {
this.service.delete(this.providerName()!, this.providerKey()).subscribe(() => {
this.toasterService.success('AbpFeatureManagement::ResetedToDefault');
this.visible = false;
if (!this.providerKey) {
if (!this.providerKey()) {
// to refresh host's features
this.configState.refreshAppState().subscribe();
}
@ -190,17 +201,18 @@ export class FeatureManagementComponent
isParentDisabled(parentName: string, groupName: string, provider: string): boolean {
const children = this.features[groupName]?.filter(f => f.parentName === parentName);
const providerNameValue = this.providerName();
if (children?.length) {
return children.some(child => {
const childProvider = child.provider?.name;
return (
(childProvider !== this.providerName && childProvider !== this.defaultProviderName) ||
(provider !== this.providerName && provider !== this.defaultProviderName)
(childProvider !== providerNameValue && childProvider !== this.defaultProviderName) ||
(provider !== providerNameValue && provider !== this.defaultProviderName)
);
});
} else {
return provider !== this.providerName && provider !== this.defaultProviderName;
return provider !== providerNameValue && provider !== this.defaultProviderName;
}
}

9
npm/ng-packs/packages/feature-management/src/lib/models/feature-management.ts

@ -1,13 +1,14 @@
import { EventEmitter } from '@angular/core';
import { InputSignal, OutputEmitterRef } from '@angular/core';
export namespace FeatureManagement {
export interface FeatureManagementComponentInputs {
visible: boolean;
readonly providerName: string;
readonly providerKey: string;
readonly providerName: InputSignal<string | undefined>;
readonly providerKey: InputSignal<string | undefined>;
}
export interface FeatureManagementComponentOutputs {
readonly visibleChange: EventEmitter<boolean>;
readonly visibleChange: OutputEmitterRef<boolean>;
}
}

6
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html

@ -1,9 +1,9 @@
<abp-modal [(visible)]="visible" [busy]="modalBusy" [options]="{ size: 'lg', scrollable: false }">
@if (data.entityDisplayName || entityDisplayName) {
@if (data.entityDisplayName || entityDisplayName()) {
<ng-template #abpHeader>
<h4>
{{ 'AbpPermissionManagement::Permissions' | abpLocalization }} -
{{ entityDisplayName || data.entityDisplayName }}
{{ entityDisplayName() || data.entityDisplayName }}
</h4>
</ng-template>
<ng-template #abpBody>
@ -115,7 +115,7 @@
/>
<label class="form-check-label" [attr.for]="permission.name"
>{{ permission.displayName }}
@if (!hideBadges) {
@if (!hideBadges()) {
@for (provider of permission.grantedProviders; track $index) {
<span class="badge bg-primary text-dark"
>{{ provider.providerName }}: {{ provider.providerKey }}</span

130
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts

@ -19,14 +19,14 @@ import {
computed,
DOCUMENT,
ElementRef,
EventEmitter,
inject,
Input,
Output,
input,
output,
QueryList,
signal,
TrackByFunction,
ViewChildren,
effect,
} from '@angular/core';
import { concat, of } from 'rxjs';
import { finalize, switchMap, take, tap } from 'rxjs/operators';
@ -118,49 +118,18 @@ export class PermissionManagementComponent
protected readonly toasterService = inject(ToasterService);
private document = inject(DOCUMENT);
@Input()
readonly providerName!: string;
// Signal inputs
readonly providerName = input.required<string>();
readonly providerKey = input.required<string>();
readonly hideBadges = input(false);
readonly entityDisplayName = input<string | undefined>(undefined);
readonly visibleInput = input(false, { alias: 'visible' });
@Input()
readonly providerKey!: string;
// Output signals
readonly visibleChange = output<boolean>();
@Input()
readonly hideBadges = false;
protected _visible = false;
@Input()
entityDisplayName: string | undefined;
@Input()
get visible(): boolean {
return this._visible;
}
set visible(value: boolean) {
if (value === this._visible) {
return;
}
if (value) {
this.openModal().subscribe(() => {
this._visible = true;
this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes)
.pipe(take(1))
.subscribe(() => {
this.initModal();
});
});
} else {
this.setSelectedGroup(null);
this._visible = false;
this.visibleChange.emit(false);
this.filter.set('');
}
}
@Output() readonly visibleChange = new EventEmitter<boolean>();
// Internal state
protected readonly _visible = signal(false);
@ViewChildren('selectAllInThisTabsRef')
selectAllInThisTabsRef!: QueryList<ElementRef<HTMLInputElement>>;
@ -216,6 +185,59 @@ export class PermissionManagementComponent
trackByFn: TrackByFunction<PermissionGroupDto> = (_, item) => item.name;
// Getter/setter for backward compatibility
get visible(): boolean {
return this._visible();
}
set visible(value: boolean) {
if (value === this._visible()) {
return;
}
if (value) {
this.openModal().subscribe(() => {
this._visible.set(true);
this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes)
.pipe(take(1))
.subscribe(() => {
this.initModal();
});
});
} else {
this.setSelectedGroup(null);
this._visible.set(false);
this.visibleChange.emit(false);
this.filter.set('');
}
}
constructor() {
// Sync visible input to internal signal
effect(() => {
const inputValue = this.visibleInput();
if (this._visible() !== inputValue) {
if (inputValue) {
this.openModal().subscribe(() => {
this._visible.set(true);
this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes)
.pipe(take(1))
.subscribe(() => {
this.initModal();
});
});
} else {
this.setSelectedGroup(null);
this._visible.set(false);
this.visibleChange.emit(false);
this.filter.set('');
}
}
});
}
getChecked(name: string) {
return (this.permissions.find(per => per.name === name) || { isGranted: false }).isGranted;
}
@ -249,7 +271,7 @@ export class PermissionManagementComponent
this.disableSelectAllTab = permissions.every(
permission =>
permission.isGranted &&
permission.grantedProviders?.every(p => p.providerName !== this.providerName),
permission.grantedProviders?.every(p => p.providerName !== this.providerName()),
);
} else {
this.disableSelectAllTab = false;
@ -258,7 +280,7 @@ export class PermissionManagementComponent
isGrantedByOtherProviderName(grantedProviders: ProviderInfoDto[]): boolean {
if (grantedProviders.length) {
return grantedProviders.findIndex(p => p.providerName !== this.providerName) > -1;
return grantedProviders.findIndex(p => p.providerName !== this.providerName()) > -1;
}
return false;
}
@ -348,7 +370,7 @@ export class PermissionManagementComponent
setTabCheckboxState() {
const selectablePermissions = this.selectedGroupPermissions.filter(per =>
per.grantedProviders.every(p => p.providerName === this.providerName),
per.grantedProviders.every(p => p.providerName === this.providerName()),
);
const selectedPermissions = selectablePermissions.filter(per => per.isGranted);
@ -370,7 +392,7 @@ export class PermissionManagementComponent
setGrantCheckboxState() {
const selectablePermissions = this.permissions.filter(per =>
per.grantedProviders.every(p => p.providerName === this.providerName),
per.grantedProviders.every(p => p.providerName === this.providerName()),
);
const selectedAllPermissions = selectablePermissions.filter(per => per.isGranted);
const checkboxElement = this.document.querySelector('#select-all-in-all-tabs') as any;
@ -456,7 +478,7 @@ export class PermissionManagementComponent
this.modalBusy = true;
this.service
.update(this.providerName, this.providerKey, { permissions: changedPermissions })
.update(this.providerName(), this.providerKey(), { permissions: changedPermissions })
.pipe(
switchMap(() =>
this.shouldFetchAppConfig() ? this.configState.refreshAppState() : of(null),
@ -470,11 +492,11 @@ export class PermissionManagementComponent
}
openModal() {
if (!this.providerKey || !this.providerName) {
if (!this.providerKey() || !this.providerName()) {
throw new Error('Provider Key and Provider Name are required.');
}
return this.service.get(this.providerName, this.providerKey).pipe(
return this.service.get(this.providerName(), this.providerKey()).pipe(
tap((permissionRes: GetPermissionListResultDto) => {
const { groups } = permissionRes || {};
@ -486,7 +508,7 @@ export class PermissionManagementComponent
this.disabledSelectAllInAllTabs = this.permissions.every(
per =>
per.isGranted &&
per.grantedProviders.every(provider => provider.providerName !== this.providerName),
per.grantedProviders.every(provider => provider.providerName !== this.providerName()),
);
}),
);
@ -511,9 +533,9 @@ export class PermissionManagementComponent
shouldFetchAppConfig() {
const currentUser = this.configState.getOne('currentUser') as CurrentUserDto;
if (this.providerName === 'R') return currentUser.roles.some(role => role === this.providerKey);
if (this.providerName() === 'R') return currentUser.roles.some(role => role === this.providerKey());
if (this.providerName === 'U') return currentUser.id === this.providerKey;
if (this.providerName() === 'U') return currentUser.id === this.providerKey();
return false;
}

11
npm/ng-packs/packages/permission-management/src/lib/models/permission-management.ts

@ -1,5 +1,5 @@
import { GetPermissionListResultDto } from '@abp/ng.permission-management/proxy';
import { EventEmitter } from '@angular/core';
import { InputSignal, OutputEmitterRef } from '@angular/core';
export namespace PermissionManagement {
export interface State {
@ -8,12 +8,13 @@ export namespace PermissionManagement {
export interface PermissionManagementComponentInputs {
visible: boolean;
readonly providerName: string;
readonly providerKey: string;
readonly hideBadges: boolean;
readonly providerName: InputSignal<string>;
readonly providerKey: InputSignal<string>;
readonly hideBadges: InputSignal<boolean>;
}
export interface PermissionManagementComponentOutputs {
readonly visibleChange: EventEmitter<boolean>;
readonly visibleChange: OutputEmitterRef<boolean>;
}
}

Loading…
Cancel
Save