mirror of https://github.com/abpframework/abp.git
22 changed files with 677 additions and 104 deletions
@ -0,0 +1,64 @@ |
|||
<abp-modal [visible]="true" (visibleChange)="onVisibleChange($event)"> |
|||
<ng-template #abpHeader> |
|||
<h3>{{ 'CmsKit::Features' | abpLocalization }}</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<div formArrayName="features"> |
|||
@for (featureControl of featuresFormArray.controls; track $index; let i = $index) { |
|||
<div class="mb-3" [formGroupName]="i"> |
|||
@if (featureControl.get('isAvailable')?.value) { |
|||
<div class="form-check"> |
|||
<input |
|||
type="checkbox" |
|||
class="form-check-input" |
|||
id="feature-{{ i }}" |
|||
formControlName="isEnabled" |
|||
/> |
|||
<label class="form-check-label" for="feature-{{ i }}"> |
|||
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }} |
|||
</label> |
|||
</div> |
|||
} @else { |
|||
<div |
|||
data-toggle="tooltip" |
|||
[title]="'CmsKit::BlogFeatureNotAvailable' | abpLocalization" |
|||
> |
|||
<div class="form-check"> |
|||
<input |
|||
type="checkbox" |
|||
class="form-check-input" |
|||
id="feature-{{ i }}" |
|||
formControlName="isEnabled" |
|||
disabled |
|||
/> |
|||
<label class="form-check-label" for="feature-{{ i }}"> |
|||
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }} |
|||
</label> |
|||
</div> |
|||
</div> |
|||
} |
|||
<input type="hidden" formControlName="featureName" /> |
|||
<input type="hidden" formControlName="isAvailable" /> |
|||
</div> |
|||
} |
|||
</div> |
|||
</form> |
|||
} @else { |
|||
<div class="text-center"> |
|||
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i> |
|||
</div> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,134 @@ |
|||
import { Component, OnInit, inject, input, output } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormArray, FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { forkJoin } from 'rxjs'; |
|||
import { |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { |
|||
BlogFeatureAdminService, |
|||
BlogFeatureDto, |
|||
BlogFeatureInputDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
export interface BlogFeaturesModalVisibleChange { |
|||
visible: boolean; |
|||
refresh: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-features-modal', |
|||
templateUrl: './blog-features-modal.component.html', |
|||
imports: [ |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
], |
|||
}) |
|||
export class BlogFeaturesModalComponent implements OnInit { |
|||
private blogFeatureService = inject(BlogFeatureAdminService); |
|||
private fb = inject(FormBuilder); |
|||
private toasterService = inject(ToasterService); |
|||
|
|||
blogId = input<string>(); |
|||
visibleChange = output<BlogFeaturesModalVisibleChange>(); |
|||
|
|||
form: FormGroup; |
|||
features: BlogFeatureDto[] = []; |
|||
private initialFeatureStates: Map<string, boolean> = new Map(); |
|||
|
|||
ngOnInit() { |
|||
if (this.blogId()) { |
|||
this.loadFeatures(); |
|||
} |
|||
} |
|||
|
|||
private loadFeatures() { |
|||
this.blogFeatureService.getList(this.blogId()!).subscribe(features => { |
|||
this.features = features.sort((a, b) => |
|||
(a.featureName || '').localeCompare(b.featureName || ''), |
|||
); |
|||
// Store initial states
|
|||
this.initialFeatureStates = new Map( |
|||
this.features.map(f => [f.featureName || '', f.isEnabled || false]), |
|||
); |
|||
this.buildForm(); |
|||
}); |
|||
} |
|||
|
|||
private buildForm() { |
|||
const featureControls = this.features.map(feature => |
|||
this.fb.group({ |
|||
featureName: [feature.featureName], |
|||
isEnabled: [feature.isEnabled], |
|||
isAvailable: [(feature as any).isAvailable ?? true], |
|||
}), |
|||
); |
|||
|
|||
this.form = this.fb.group({ |
|||
features: this.fb.array(featureControls), |
|||
}); |
|||
} |
|||
|
|||
get featuresFormArray(): FormArray { |
|||
return this.form.get('features') as FormArray; |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean, refresh = false) { |
|||
this.visibleChange.emit({ visible, refresh }); |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid || !this.blogId()) { |
|||
return; |
|||
} |
|||
|
|||
const featuresArray = this.form.get('features') as FormArray; |
|||
|
|||
// Only save features that have changed
|
|||
const changedFeatures: BlogFeatureInputDto[] = featuresArray.controls |
|||
.map(control => { |
|||
const featureName = control.get('featureName')?.value; |
|||
const isEnabled = control.get('isEnabled')?.value; |
|||
const initialIsEnabled = this.initialFeatureStates.get(featureName); |
|||
|
|||
// Only include if the value has changed
|
|||
if (featureName && initialIsEnabled !== isEnabled) { |
|||
return { |
|||
featureName, |
|||
isEnabled, |
|||
}; |
|||
} |
|||
return null; |
|||
}) |
|||
.filter((input): input is BlogFeatureInputDto => input !== null); |
|||
|
|||
// If no features changed, just close the modal
|
|||
if (changedFeatures.length === 0) { |
|||
this.onVisibleChange(false, false); |
|||
return; |
|||
} |
|||
|
|||
// Save only changed features
|
|||
const saveObservables = changedFeatures.map(input => |
|||
this.blogFeatureService.set(this.blogId()!, input), |
|||
); |
|||
|
|||
// Use forkJoin to save all changed features at once
|
|||
forkJoin(saveObservables).subscribe({ |
|||
next: () => { |
|||
this.onVisibleChange(false, true); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<abp-page [title]="'CmsKit::Blogs' | abpLocalization" [toolbar]="data.items"> |
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
|
|||
@if (isModalVisible) { |
|||
<abp-blog-modal [selected]="selected" (visibleChange)="onVisibleModalChange($event)" /> |
|||
} |
|||
|
|||
@if (isFeaturesModalVisible) { |
|||
<abp-blog-features-modal |
|||
[blogId]="selectedBlogId" |
|||
(visibleChange)="onFeaturesModalChange($event)" |
|||
/> |
|||
} |
|||
</abp-page> |
|||
@ -0,0 +1,112 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; |
|||
import { ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { BlogAdminService, BlogGetListInput, BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { BlogModalComponent, BlogModalVisibleChange } from '../blog-modal/blog-modal.component'; |
|||
import { |
|||
BlogFeaturesModalComponent, |
|||
BlogFeaturesModalVisibleChange, |
|||
} from '../blog-features-modal/blog-features-modal.component'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-list', |
|||
templateUrl: './blog-list.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.Blogs, |
|||
}, |
|||
], |
|||
imports: [ |
|||
ExtensibleTableComponent, |
|||
PageComponent, |
|||
LocalizationPipe, |
|||
FormsModule, |
|||
CommonModule, |
|||
BlogModalComponent, |
|||
BlogFeaturesModalComponent, |
|||
], |
|||
}) |
|||
export class BlogListComponent implements OnInit { |
|||
data: PagedResultDto<BlogDto> = { items: [], totalCount: 0 }; |
|||
|
|||
public readonly list = inject(ListService<BlogGetListInput>); |
|||
private blogService = inject(BlogAdminService); |
|||
|
|||
filter = ''; |
|||
isModalVisible = false; |
|||
selected?: BlogDto; |
|||
isFeaturesModalVisible = false; |
|||
selectedBlogId?: string; |
|||
|
|||
ngOnInit() { |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
onSearch() { |
|||
this.list.filter = this.filter; |
|||
this.list.get(); |
|||
} |
|||
|
|||
add() { |
|||
this.selected = {} as BlogDto; |
|||
this.isModalVisible = true; |
|||
} |
|||
|
|||
edit(id: string) { |
|||
this.blogService.get(id).subscribe(blog => { |
|||
this.selected = blog; |
|||
this.isModalVisible = true; |
|||
}); |
|||
} |
|||
|
|||
openFeatures(id: string) { |
|||
this.selectedBlogId = id; |
|||
this.isFeaturesModalVisible = true; |
|||
} |
|||
|
|||
private hookToQuery() { |
|||
this.list |
|||
.hookToQuery(query => { |
|||
let filters: Partial<BlogGetListInput> = {}; |
|||
if (this.list.filter) { |
|||
filters.filter = this.list.filter; |
|||
} |
|||
const input: BlogGetListInput = { |
|||
...query, |
|||
...filters, |
|||
}; |
|||
return this.blogService.getList(input); |
|||
}) |
|||
.subscribe(res => { |
|||
this.data = res; |
|||
}); |
|||
} |
|||
|
|||
onVisibleModalChange(visibilityChange: BlogModalVisibleChange) { |
|||
if (visibilityChange.visible) { |
|||
return; |
|||
} |
|||
if (visibilityChange.refresh) { |
|||
this.list.get(); |
|||
} |
|||
this.selected = null; |
|||
this.isModalVisible = false; |
|||
} |
|||
|
|||
onFeaturesModalChange(visibilityChange: BlogFeaturesModalVisibleChange) { |
|||
if (visibilityChange.visible) { |
|||
return; |
|||
} |
|||
if (visibilityChange.refresh) { |
|||
this.list.get(); |
|||
} |
|||
this.selectedBlogId = null; |
|||
this.isFeaturesModalVisible = false; |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<abp-modal [visible]="true" (visibleChange)="onVisibleChange($event)"> |
|||
<ng-template #abpHeader> |
|||
<h3> |
|||
{{ (selected()?.id ? 'AbpUi::Edit' : 'AbpUi::New') | abpLocalization }} |
|||
</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<abp-extensible-form [selectedRecord]="selected()" /> |
|||
</form> |
|||
} @else { |
|||
<div class="text-center"> |
|||
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i> |
|||
</div> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,86 @@ |
|||
import { Component, OnInit, inject, Injector, input, output, DestroyRef } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormGroup } 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 { BlogAdminService, BlogDto, CreateBlogDto, UpdateBlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { prepareSlugFromControl } from '@abp/ng.cms-kit'; |
|||
|
|||
export interface BlogModalVisibleChange { |
|||
visible: boolean; |
|||
refresh: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-modal', |
|||
templateUrl: './blog-modal.component.html', |
|||
imports: [ |
|||
ExtensibleFormComponent, |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
], |
|||
}) |
|||
export class BlogModalComponent implements OnInit { |
|||
private blogService = inject(BlogAdminService); |
|||
private injector = inject(Injector); |
|||
private toasterService = inject(ToasterService); |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
selected = input<BlogDto>(); |
|||
visibleChange = output<BlogModalVisibleChange>(); |
|||
|
|||
form: FormGroup; |
|||
|
|||
ngOnInit() { |
|||
this.buildForm(); |
|||
} |
|||
|
|||
private buildForm() { |
|||
const data = new FormPropData(this.injector, this.selected()); |
|||
this.form = generateFormFromProps(data); |
|||
prepareSlugFromControl(this.form, 'name', 'slug', this.destroyRef); |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean, refresh = false) { |
|||
this.visibleChange.emit({ visible, refresh }); |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid) { |
|||
return; |
|||
} |
|||
|
|||
let observable$ = this.blogService.create(this.form.value as CreateBlogDto); |
|||
|
|||
const selectedBlog = this.selected(); |
|||
const { id } = selectedBlog || {}; |
|||
|
|||
if (id) { |
|||
observable$ = this.blogService.update(id, { |
|||
...selectedBlog, |
|||
...this.form.value, |
|||
} as UpdateBlogDto); |
|||
} |
|||
|
|||
observable$.subscribe(() => { |
|||
this.onVisibleChange(false, true); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './blog-list/blog-list.component'; |
|||
export * from './blog-modal/blog-modal.component'; |
|||
export * from './blog-features-modal/blog-features-modal.component'; |
|||
@ -1,7 +1,7 @@ |
|||
export * from './comments'; |
|||
export * from './tags'; |
|||
export * from './pages'; |
|||
// export * from './blogs';
|
|||
export * from './blogs'; |
|||
// export * from './blog-posts';
|
|||
// export * from './menus';
|
|||
// export * from './global-resources';
|
|||
|
|||
@ -0,0 +1,22 @@ |
|||
import { CreateBlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { FormProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { Validators } from '@angular/forms'; |
|||
|
|||
export const DEFAULT_BLOG_CREATE_FORM_PROPS = FormProp.createMany<CreateBlogDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'name', |
|||
displayName: 'CmsKit::Name', |
|||
id: 'name', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
id: 'slug', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_BLOG_EDIT_FORM_PROPS = DEFAULT_BLOG_CREATE_FORM_PROPS; |
|||
@ -0,0 +1,48 @@ |
|||
import { BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityAction } from '@abp/ng.components/extensible'; |
|||
import { BlogAdminService } from '@abp/ng.cms-kit/proxy'; |
|||
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { BlogListComponent } from '../../components/blogs/blog-list/blog-list.component'; |
|||
|
|||
export const DEFAULT_BLOG_ENTITY_ACTIONS = EntityAction.createMany<BlogDto>([ |
|||
{ |
|||
text: 'CmsKit::Features', |
|||
action: data => { |
|||
const component = data.getInjected(BlogListComponent); |
|||
component.openFeatures(data.record.id!); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Features', |
|||
}, |
|||
{ |
|||
text: 'AbpUi::Edit', |
|||
action: data => { |
|||
const component = data.getInjected(BlogListComponent); |
|||
component.edit(data.record.id!); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Update', |
|||
}, |
|||
{ |
|||
text: 'AbpUi::Delete', |
|||
action: data => { |
|||
const blogService = data.getInjected(BlogAdminService); |
|||
const confirmationService = data.getInjected(ConfirmationService); |
|||
const list = data.getInjected(ListService); |
|||
|
|||
confirmationService |
|||
.warn('CmsKit::BlogDeletionConfirmationMessage', 'AbpUi::AreYouSure', { |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
messageLocalizationParams: [data.record.name || ''], |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
blogService.delete(data.record.id!).subscribe(() => { |
|||
list.get(); |
|||
}); |
|||
} |
|||
}); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Delete', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,26 @@ |
|||
import { BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityProp, ePropType } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_BLOG_ENTITY_PROPS = EntityProp.createMany<BlogDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'name', |
|||
displayName: 'CmsKit::Name', |
|||
sortable: true, |
|||
columnWidth: 250, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
sortable: true, |
|||
columnWidth: 250, |
|||
}, |
|||
{ |
|||
type: ePropType.Number, |
|||
name: 'blogPostCount', |
|||
displayName: 'CmsKit::BlogPostCount', |
|||
sortable: true, |
|||
columnWidth: 150, |
|||
}, |
|||
]); |
|||
@ -0,0 +1,15 @@ |
|||
import { BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ToolbarAction } from '@abp/ng.components/extensible'; |
|||
import { BlogListComponent } from '../../components/blogs/blog-list/blog-list.component'; |
|||
|
|||
export const DEFAULT_BLOG_TOOLBAR_ACTIONS = ToolbarAction.createMany<BlogDto[]>([ |
|||
{ |
|||
text: 'CmsKit::NewBlog', |
|||
action: data => { |
|||
const component = data.getInjected(BlogListComponent); |
|||
component.add(); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Create', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,4 @@ |
|||
export * from './default-blog-entity-actions'; |
|||
export * from './default-blog-entity-props'; |
|||
export * from './default-blog-toolbar-actions'; |
|||
export * from './default-blog-create-form-props'; |
|||
Loading…
Reference in new issue