mirror of https://github.com/abpframework/abp.git
14 changed files with 632 additions and 1 deletions
@ -0,0 +1,2 @@ |
|||
export * from './menu-item-list/menu-item-list.component'; |
|||
export * from './menu-item-modal/menu-item-modal.component'; |
|||
@ -0,0 +1,56 @@ |
|||
<abp-page [title]="'CmsKit::MenuItems' | abpLocalization" [toolbar]="nodes"> |
|||
<div class="card"> |
|||
<div class="card-body"> |
|||
@if (nodes.length > 0) { |
|||
<abp-tree |
|||
[nodes]="nodes" |
|||
[draggable]="draggable" |
|||
[expandedKeys]="expandedKeys" |
|||
[selectedNode]="selectedNode" |
|||
(selectedNodeChange)="onSelectedNodeChange($event)" |
|||
[beforeDrop]="beforeDrop" |
|||
(dropOver)="onDrop($event)" |
|||
> |
|||
<ng-template #menu let-node> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="edit(node.key)" |
|||
*abpPermission="'CmsKit.Menus.Update'" |
|||
> |
|||
<i class="fa fa-pencil me-2"></i> |
|||
{{ 'AbpUi::Edit' | abpLocalization }} |
|||
</button> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="addSubMenuItem(node.key)" |
|||
*abpPermission="'CmsKit.Menus.Create'" |
|||
> |
|||
<i class="fa fa-plus me-2"></i> |
|||
{{ 'CmsKit::AddSubMenuItem' | abpLocalization }} |
|||
</button> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="delete(node.key, node.title)" |
|||
*abpPermission="'CmsKit.Menus.Delete'" |
|||
> |
|||
<i class="fa fa-remove me-2"></i> |
|||
{{ 'AbpUi::Delete' | abpLocalization }} |
|||
</button> |
|||
</ng-template> |
|||
</abp-tree> |
|||
} @else { |
|||
<div class="text-muted text-center"> |
|||
{{ 'CmsKit::NoMenuItems' | abpLocalization }} |
|||
</div> |
|||
} |
|||
</div> |
|||
</div> |
|||
</abp-page> |
|||
|
|||
@if (isModalVisible) { |
|||
<abp-menu-item-modal |
|||
[selected]="selectedMenuItem || undefined" |
|||
[parentId]="parentId" |
|||
(visibleChange)="onVisibleModalChange($event)" |
|||
/> |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { of } from 'rxjs'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { ListService, LocalizationPipe, PermissionDirective } from '@abp/ng.core'; |
|||
import { TreeComponent } from '@abp/ng.components/tree'; |
|||
import { EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; |
|||
import { |
|||
MenuItemAdminService, |
|||
MenuItemDto, |
|||
MenuItemWithDetailsDto, |
|||
MenuItemMoveInput, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { |
|||
MenuItemModalComponent, |
|||
MenuItemModalVisibleChange, |
|||
} from '../menu-item-modal/menu-item-modal.component'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-menu-item-list', |
|||
templateUrl: './menu-item-list.component.html', |
|||
imports: [ |
|||
PageComponent, |
|||
TreeComponent, |
|||
LocalizationPipe, |
|||
CommonModule, |
|||
MenuItemModalComponent, |
|||
PermissionDirective, |
|||
], |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.Menus, |
|||
}, |
|||
], |
|||
}) |
|||
export class MenuItemListComponent implements OnInit { |
|||
private menuItemService = inject(MenuItemAdminService); |
|||
private confirmationService = inject(ConfirmationService); |
|||
|
|||
nodes: any[] = []; |
|||
selectedNode: MenuItemDto | null = null; |
|||
expandedKeys: string[] = []; |
|||
draggable = true; |
|||
isModalVisible = false; |
|||
selectedMenuItem: MenuItemDto | MenuItemWithDetailsDto | null = null; |
|||
parentId: string | null = null; |
|||
|
|||
ngOnInit() { |
|||
this.loadMenuItems(); |
|||
} |
|||
|
|||
private loadMenuItems() { |
|||
this.menuItemService.getList().subscribe(result => { |
|||
if (result.items && result.items.length > 0) { |
|||
this.nodes = this.buildTreeNodes(result.items); |
|||
// Expand all nodes by default
|
|||
this.expandedKeys = this.nodes.map(n => n.key); |
|||
} else { |
|||
this.nodes = []; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private buildTreeNodes(items: MenuItemDto[]): any[] { |
|||
const nodeMap = new Map<string, any>(); |
|||
const rootNodes: any[] = []; |
|||
|
|||
// First pass: create all nodes
|
|||
items.forEach(item => { |
|||
const node: any = { |
|||
key: item.id, |
|||
title: item.displayName || '', |
|||
entity: item, |
|||
children: [], |
|||
isLeaf: false, |
|||
}; |
|||
nodeMap.set(item.id!, node); |
|||
}); |
|||
|
|||
// Second pass: build tree structure
|
|||
items.forEach(item => { |
|||
const node = nodeMap.get(item.id!); |
|||
if (item.parentId) { |
|||
const parent = nodeMap.get(item.parentId); |
|||
if (parent) { |
|||
parent.children.push(node); |
|||
parent.isLeaf = false; |
|||
} else { |
|||
rootNodes.push(node); |
|||
} |
|||
} else { |
|||
rootNodes.push(node); |
|||
} |
|||
}); |
|||
|
|||
// Sort by order
|
|||
const sortByOrder = (nodes: any[]) => { |
|||
nodes.sort((a, b) => (a.entity.order || 0) - (b.entity.order || 0)); |
|||
nodes.forEach(node => { |
|||
if (node.children && node.children.length > 0) { |
|||
sortByOrder(node.children); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
sortByOrder(rootNodes); |
|||
return rootNodes; |
|||
} |
|||
|
|||
onSelectedNodeChange(node: any) { |
|||
this.selectedNode = node?.entity || null; |
|||
} |
|||
|
|||
onDrop(event: any) { |
|||
const node = event.dragNode?.origin?.entity; |
|||
if (!node) { |
|||
return; |
|||
} |
|||
|
|||
const newParentId = event.dragNode?.parent?.key === '0' ? null : event.dragNode?.parent?.key; |
|||
const position = event.dragNode?.pos || 0; |
|||
|
|||
const parentNodeName = |
|||
!newParentId || newParentId === '0' |
|||
? 'Root' |
|||
: event.dragNode?.parent?.origin?.entity?.displayName || 'Root'; |
|||
|
|||
this.confirmationService |
|||
.warn('CmsKit::MenuItemMoveConfirmMessage', 'AbpUi::AreYouSure', { |
|||
messageLocalizationParams: [node.displayName || '', parentNodeName], |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
const input: MenuItemMoveInput = { |
|||
newParentId: newParentId === '0' ? null : newParentId, |
|||
position: position, |
|||
}; |
|||
|
|||
this.menuItemService.moveMenuItem(node.id!, input).subscribe({ |
|||
next: () => { |
|||
this.loadMenuItems(); |
|||
}, |
|||
error: () => { |
|||
// Reload to rollback
|
|||
this.loadMenuItems(); |
|||
}, |
|||
}); |
|||
} else { |
|||
// Reload to rollback
|
|||
this.loadMenuItems(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
beforeDrop = (event: any) => { |
|||
return of(true); |
|||
}; |
|||
|
|||
add() { |
|||
this.selectedMenuItem = null; |
|||
this.parentId = null; |
|||
this.isModalVisible = true; |
|||
} |
|||
|
|||
addSubMenuItem(parentId?: string) { |
|||
this.selectedMenuItem = null; |
|||
this.parentId = parentId || null; |
|||
this.isModalVisible = true; |
|||
} |
|||
|
|||
edit(id: string) { |
|||
this.menuItemService.get(id).subscribe(menuItem => { |
|||
this.selectedMenuItem = menuItem; |
|||
this.parentId = null; |
|||
this.isModalVisible = true; |
|||
}); |
|||
} |
|||
|
|||
onVisibleModalChange(visibilityChange: MenuItemModalVisibleChange) { |
|||
if (visibilityChange.visible) { |
|||
return; |
|||
} |
|||
if (visibilityChange.refresh) { |
|||
this.loadMenuItems(); |
|||
} |
|||
this.selectedMenuItem = null; |
|||
this.parentId = null; |
|||
this.isModalVisible = false; |
|||
} |
|||
|
|||
delete(id: string, displayName?: string) { |
|||
this.confirmationService |
|||
.warn('CmsKit::MenuItemDeletionConfirmationMessage', 'AbpUi::AreYouSure', { |
|||
messageLocalizationParams: [displayName || ''], |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.menuItemService.delete(id).subscribe({ |
|||
next: () => { |
|||
this.loadMenuItems(); |
|||
}, |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
<abp-modal [visible]="visible()" (visibleChange)="onVisibleChange($event)"> |
|||
<ng-template #abpHeader> |
|||
<h3> |
|||
@if (selected()?.id) { |
|||
{{ 'AbpUi::Edit' | abpLocalization }} |
|||
} @else { |
|||
{{ 'AbpUi::New' | abpLocalization }} |
|||
} |
|||
</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<ul ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeTab"> |
|||
<li ngbNavItem> |
|||
<a ngbNavLink>{{ 'CmsKit::Url' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<label class="form-label">{{ 'CmsKit::Url' | abpLocalization }}</label> |
|||
<input type="text" class="form-control" formControlName="url" /> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
<li ngbNavItem> |
|||
<a ngbNavLink>{{ 'CmsKit::Page' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<label class="form-label">{{ 'CmsKit::Page' | abpLocalization }}</label> |
|||
<select class="form-control" formControlName="pageId"> |
|||
<option [ngValue]="null"></option> |
|||
@for (page of pages; track page.id) { |
|||
<option [ngValue]="page.id">{{ page.title }}</option> |
|||
} |
|||
</select> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
</ul> |
|||
<div class="mt-2 fade-in-top" [ngbNavOutlet]="nav"></div> |
|||
<hr /> |
|||
<abp-extensible-form [selectedRecord]="selected() || {}" /> |
|||
</form> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button (click)="save()" [disabled]="form?.invalid"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,162 @@ |
|||
import { Component, OnInit, inject, Injector, input, output } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ExtensibleFormComponent, |
|||
FormPropData, |
|||
generateFormFromProps, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { |
|||
MenuItemAdminService, |
|||
MenuItemDto, |
|||
MenuItemWithDetailsDto, |
|||
MenuItemCreateInput, |
|||
MenuItemUpdateInput, |
|||
PageLookupDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { forkJoin } from 'rxjs'; |
|||
|
|||
export interface MenuItemModalVisibleChange { |
|||
visible: boolean; |
|||
refresh: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-menu-item-modal', |
|||
templateUrl: './menu-item-modal.component.html', |
|||
imports: [ |
|||
ExtensibleFormComponent, |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
NgbNavModule, |
|||
], |
|||
}) |
|||
export class MenuItemModalComponent implements OnInit { |
|||
private menuItemService = inject(MenuItemAdminService); |
|||
private injector = inject(Injector); |
|||
private toasterService = inject(ToasterService); |
|||
|
|||
selected = input<MenuItemWithDetailsDto | MenuItemDto>(); |
|||
parentId = input<string | null>(); |
|||
visible = input<boolean>(true); |
|||
visibleChange = output<MenuItemModalVisibleChange>(); |
|||
|
|||
form: FormGroup; |
|||
activeTab: 'url' | 'page' = 'url'; |
|||
pages: PageLookupDto[] = []; |
|||
selectedPage: PageLookupDto | null = null; |
|||
|
|||
ngOnInit() { |
|||
const selectedItem = this.selected(); |
|||
|
|||
if (selectedItem?.id) { |
|||
// Load menu item and pages in parallel
|
|||
forkJoin({ |
|||
menuItem: this.menuItemService.get(selectedItem.id), |
|||
pages: this.menuItemService.getPageLookup({ |
|||
maxResultCount: 1000, |
|||
}), |
|||
}).subscribe(({ menuItem, pages }) => { |
|||
this.pages = pages.items || []; |
|||
this.buildForm(menuItem); |
|||
if (menuItem.pageId) { |
|||
this.activeTab = 'page'; |
|||
this.selectedPage = this.pages.find(p => p.id === menuItem.pageId) || null; |
|||
} |
|||
}); |
|||
} else { |
|||
// Load pages for create mode
|
|||
this.loadPages(); |
|||
this.buildForm(); |
|||
} |
|||
} |
|||
|
|||
private loadPages() { |
|||
this.menuItemService |
|||
.getPageLookup({ |
|||
maxResultCount: 1000, |
|||
}) |
|||
.subscribe(result => { |
|||
this.pages = result.items || []; |
|||
}); |
|||
} |
|||
|
|||
private buildForm(menuItem?: MenuItemWithDetailsDto | MenuItemDto) { |
|||
const data = new FormPropData(this.injector, menuItem || {}); |
|||
const baseForm = generateFormFromProps(data); |
|||
|
|||
const parentId = this.parentId() || menuItem?.parentId || null; |
|||
|
|||
this.form = new FormGroup({ |
|||
...baseForm.controls, |
|||
url: new FormControl(menuItem?.url || ''), |
|||
pageId: new FormControl(menuItem?.pageId || null), |
|||
}); |
|||
|
|||
if (parentId === null && !menuItem?.id) { |
|||
this.menuItemService.getAvailableMenuOrder().subscribe(order => { |
|||
this.form.patchValue({ order }); |
|||
}); |
|||
} else if (parentId && !menuItem?.id) { |
|||
this.menuItemService.getAvailableMenuOrder(parentId).subscribe(order => { |
|||
this.form.patchValue({ order }); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean, refresh = false) { |
|||
this.visibleChange.emit({ visible, refresh }); |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid) { |
|||
return; |
|||
} |
|||
|
|||
const formValue = this.form.value; |
|||
const selectedItem = this.selected(); |
|||
|
|||
// If page is selected, clear URL; if URL is used, clear pageId
|
|||
if (this.activeTab === 'page' && formValue.pageId) { |
|||
formValue.url = ''; |
|||
} else if (this.activeTab === 'url') { |
|||
formValue.pageId = null; |
|||
} |
|||
|
|||
let observable$; |
|||
|
|||
if (selectedItem?.id) { |
|||
const updateInput: MenuItemUpdateInput = { |
|||
...formValue, |
|||
concurrencyStamp: (selectedItem as MenuItemWithDetailsDto).concurrencyStamp, |
|||
}; |
|||
observable$ = this.menuItemService.update(selectedItem.id, updateInput); |
|||
} else { |
|||
const createInput: MenuItemCreateInput = { |
|||
...formValue, |
|||
}; |
|||
observable$ = this.menuItemService.create(createInput); |
|||
} |
|||
|
|||
observable$.subscribe({ |
|||
next: () => { |
|||
this.onVisibleChange(false, true); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
@ -1,3 +1,6 @@ |
|||
export * from './comments'; |
|||
export * from './tags'; |
|||
export * from './pages'; |
|||
export * from './blogs'; |
|||
export * from './blog-posts'; |
|||
export * from './menus'; |
|||
|
|||
@ -0,0 +1,85 @@ |
|||
import { Validators } from '@angular/forms'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { |
|||
MenuItemCreateInput, |
|||
MenuItemAdminService, |
|||
PageLookupDto, |
|||
PermissionLookupDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { FormProp, ePropType } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_MENU_ITEM_CREATE_FORM_PROPS = FormProp.createMany<MenuItemCreateInput>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'parentId', |
|||
displayName: 'CmsKit::Parent', |
|||
id: 'parentId', |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'displayName', |
|||
displayName: 'CmsKit::DisplayName', |
|||
id: 'displayName', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.Boolean, |
|||
name: 'isActive', |
|||
displayName: 'CmsKit::IsActive', |
|||
id: 'isActive', |
|||
defaultValue: false, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'icon', |
|||
displayName: 'CmsKit::Icon', |
|||
id: 'icon', |
|||
}, |
|||
{ |
|||
type: ePropType.Number, |
|||
name: 'order', |
|||
displayName: 'CmsKit::Order', |
|||
id: 'order', |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'target', |
|||
displayName: 'CmsKit::Target', |
|||
id: 'target', |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'elementId', |
|||
displayName: 'CmsKit::ElementId', |
|||
id: 'elementId', |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'cssClass', |
|||
displayName: 'CmsKit::CssClass', |
|||
id: 'cssClass', |
|||
}, |
|||
{ |
|||
type: ePropType.Enum, |
|||
name: 'requiredPermissionName', |
|||
displayName: 'CmsKit::RequiredPermissionName', |
|||
id: 'requiredPermissionName', |
|||
options: data => { |
|||
const menuItemService = data.getInjected(MenuItemAdminService); |
|||
return menuItemService |
|||
.getPermissionLookup({ |
|||
filter: '', |
|||
}) |
|||
.pipe( |
|||
map((result: { items: PermissionLookupDto[] }) => |
|||
result.items.map(permission => ({ |
|||
key: permission.displayName || permission.name || '', |
|||
value: permission.name || '', |
|||
})), |
|||
), |
|||
); |
|||
}, |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_MENU_ITEM_EDIT_FORM_PROPS = DEFAULT_MENU_ITEM_CREATE_FORM_PROPS; |
|||
@ -0,0 +1,15 @@ |
|||
import { MenuItemDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ToolbarAction } from '@abp/ng.components/extensible'; |
|||
import { MenuItemListComponent } from '../../components/menus/menu-item-list/menu-item-list.component'; |
|||
|
|||
export const DEFAULT_MENU_ITEM_TOOLBAR_ACTIONS = ToolbarAction.createMany<MenuItemDto[]>([ |
|||
{ |
|||
text: 'CmsKit::NewMenuItem', |
|||
action: data => { |
|||
const component = data.getInjected(MenuItemListComponent); |
|||
component.add(); |
|||
}, |
|||
permission: 'CmsKit.Menus.Create', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,2 @@ |
|||
export * from './default-menu-item-create-form-props'; |
|||
export * from './default-menu-item-toolbar-actions'; |
|||
Loading…
Reference in new issue