Browse Source

update: some upgrade enhancements for the cms-kit

pull/25690/head
sumeyye 2 months ago
parent
commit
2cb93fbd97
  1. 15
      npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html
  2. 63
      npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts
  3. 8
      npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.html
  4. 55
      npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.ts
  5. 10
      npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.html
  6. 55
      npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.ts
  7. 12
      npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.html
  8. 79
      npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.ts
  9. 2
      npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.html
  10. 22
      npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.ts
  11. 2
      npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.html
  12. 10
      npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.ts
  13. 6
      npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.html
  14. 46
      npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.ts
  15. 2
      npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.html
  16. 34
      npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.ts
  17. 2
      npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.html
  18. 14
      npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.ts
  19. 18
      npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-list/menu-item-list.component.html
  20. 69
      npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-list/menu-item-list.component.ts
  21. 20
      npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-modal/menu-item-modal.component.html
  22. 85
      npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-modal/menu-item-modal.component.ts
  23. 4
      npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-form/page-form.component.html
  24. 41
      npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-form/page-form.component.ts
  25. 8
      npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-list/page-list.component.html
  26. 51
      npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-list/page-list.component.ts
  27. 8
      npm/ng-packs/packages/cms-kit/admin/src/components/tags/tag-list/tag-list.component.html
  28. 77
      npm/ng-packs/packages/cms-kit/admin/src/components/tags/tag-list/tag-list.component.ts
  29. 11
      npm/ng-packs/packages/cms-kit/admin/src/components/tags/tag-modal/tag-modal.component.ts
  30. 10
      npm/ng-packs/packages/cms-kit/src/components/code-mirror-editor/code-mirror-editor.component.ts
  31. 8
      npm/ng-packs/packages/cms-kit/src/components/toast-ui/toastui-editor.component.ts

15
npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html

@ -1,13 +1,13 @@
<abp-page [title]="'CmsKit::BlogPosts' | abpLocalization"> <abp-page [title]="'CmsKit::BlogPosts' | abpLocalization">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@if (form && (!isEditMode || blogPost)) { @if (form && (!isEditMode() || blogPost())) {
<form [formGroup]="form" (ngSubmit)="saveAsDraft()" [validateOnSubmit]="true"> <form [formGroup]="form" (ngSubmit)="saveAsDraft()" [validateOnSubmit]="true">
<!-- Cover Image --> <!-- Cover Image -->
<div class="mb-3"> <div class="mb-3">
@if (coverImagePreview) { @if (coverImagePreview()) {
<div class="mb-2"> <div class="mb-2">
<img [src]="coverImagePreview" height="120" alt="Cover Image" /> <img [src]="coverImagePreview()" height="120" alt="Cover Image" />
<br /> <br />
<button type="button" class="btn btn-link p-0" (click)="removeCoverImage()"> <button type="button" class="btn btn-link p-0" (click)="removeCoverImage()">
{{ 'CmsKit::RemoveCoverImage' | abpLocalization }} {{ 'CmsKit::RemoveCoverImage' | abpLocalization }}
@ -28,7 +28,7 @@
</div> </div>
<!-- Basic fields --> <!-- Basic fields -->
<abp-extensible-form [selectedRecord]="blogPost || {}" /> <abp-extensible-form [selectedRecord]="blogPost() || {}" />
<!-- Content editor --> <!-- Content editor -->
<div class="mt-3"> <div class="mt-3">
@ -38,14 +38,15 @@
</form> </form>
<!-- Tags Editor --> <!-- Tags Editor -->
@if (isTagsEnabled) { @if (isTagsEnabled()) {
<hr /> <hr />
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ 'CmsKit::Tags' | abpLocalization }}</label> <label class="form-label">{{ 'CmsKit::Tags' | abpLocalization }}</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
[(ngModel)]="tags" [ngModel]="tags()"
(ngModelChange)="tags.set($event)"
[ngModelOptions]="{ standalone: true }" [ngModelOptions]="{ standalone: true }"
[placeholder]="'CmsKit::TagsHelpText' | abpLocalization" [placeholder]="'CmsKit::TagsHelpText' | abpLocalization"
/> />
@ -63,7 +64,7 @@
<button class="btn btn-secondary" (click)="saveAsDraft()" [disabled]="form?.invalid"> <button class="btn btn-secondary" (click)="saveAsDraft()" [disabled]="form?.invalid">
{{ 'CmsKit::SaveAsDraft' | abpLocalization }} {{ 'CmsKit::SaveAsDraft' | abpLocalization }}
</button> </button>
@if (isEditMode) { @if (isEditMode()) {
<abp-button (click)="publish()" [disabled]="form?.invalid"> <abp-button (click)="publish()" [disabled]="form?.invalid">
{{ 'CmsKit::Publish' | abpLocalization }} {{ 'CmsKit::Publish' | abpLocalization }}
</abp-button> </abp-button>

63
npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts

@ -1,6 +1,14 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, inject, Injector, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
Injector,
OnInit,
signal,
} from '@angular/core';
import { ReactiveFormsModule, FormsModule, FormGroup, FormControl } from '@angular/forms'; import { ReactiveFormsModule, FormsModule, FormGroup, FormControl } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgxValidateCoreModule } from '@ngx-validate/core'; import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -29,6 +37,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { BlogPostFormService } from '../../../services'; import { BlogPostFormService } from '../../../services';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-post-form', selector: 'abp-blog-post-form',
templateUrl: './blog-post-form.component.html', templateUrl: './blog-post-form.component.html',
providers: [ providers: [
@ -60,33 +69,33 @@ export class BlogPostFormComponent implements OnInit {
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
form: FormGroup; form: FormGroup;
blogPost: BlogPostDto | null = null; readonly blogPost = signal<BlogPostDto | null>(null);
blogPostId: string | null = null; blogPostId: string | null = null;
isEditMode = false; readonly isEditMode = signal(false);
coverImageFile: File | null = null; coverImageFile: File | null = null;
coverImagePreview: string | null = null; readonly coverImagePreview = signal<string | null>(null);
tags: string = ''; readonly tags = signal('');
isTagsEnabled = true; readonly isTagsEnabled = signal(true);
readonly BLOG_POST_ENTITY_TYPE = 'BlogPost'; readonly BLOG_POST_ENTITY_TYPE = 'BlogPost';
ngOnInit() { ngOnInit() {
const id = this.route.snapshot.params['id']; const id = this.route.snapshot.params['id'];
if (id) { if (id) {
this.isEditMode = true; this.isEditMode.set(true);
this.blogPostId = id; this.blogPostId = id;
this.loadBlogPost(id); this.loadBlogPost(id);
} else { } else {
this.isEditMode = false; this.isEditMode.set(false);
this.buildForm(); this.buildForm();
} }
} }
private loadBlogPost(id: string) { private loadBlogPost(id: string) {
this.blogPostService.get(id).subscribe(blogPost => { this.blogPostService.get(id).subscribe(blogPost => {
this.blogPost = blogPost; this.blogPost.set(blogPost);
if (blogPost.coverImageMediaId) { if (blogPost.coverImageMediaId) {
this.coverImagePreview = `/api/cms-kit/media/${blogPost.coverImageMediaId}`; this.coverImagePreview.set(`/api/cms-kit/media/${blogPost.coverImageMediaId}`);
} }
this.buildForm(); this.buildForm();
this.loadTags(id); this.loadTags(id);
@ -94,7 +103,6 @@ export class BlogPostFormComponent implements OnInit {
} }
private loadTags(blogPostId: string) { private loadTags(blogPostId: string) {
// TODO: use the public service to load the tags
this.restService this.restService
.request<void, TagDto[]>({ .request<void, TagDto[]>({
method: 'GET', method: 'GET',
@ -102,28 +110,27 @@ export class BlogPostFormComponent implements OnInit {
}) })
.subscribe(tags => { .subscribe(tags => {
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
this.tags = tags.map(t => t.name || '').join(', '); this.tags.set(tags.map(t => t.name || '').join(', '));
} }
}); });
} }
private buildForm() { private buildForm() {
const data = new FormPropData(this.injector, this.blogPost || {}); const currentBlogPost = this.blogPost();
const data = new FormPropData(this.injector, currentBlogPost || {});
const baseForm = generateFormFromProps(data); const baseForm = generateFormFromProps(data);
this.form = new FormGroup({ this.form = new FormGroup({
...baseForm.controls, ...baseForm.controls,
content: new FormControl(this.blogPost?.content || ''), content: new FormControl(currentBlogPost?.content || ''),
coverImageMediaId: new FormControl(this.blogPost?.coverImageMediaId || null), coverImageMediaId: new FormControl(currentBlogPost?.coverImageMediaId || null),
}); });
prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef); prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef);
// Check if tags feature is enabled for the blog const blogId = this.form.get('blogId')?.value || currentBlogPost?.blogId;
const blogId = this.form.get('blogId')?.value || this.blogPost?.blogId;
if (blogId) { if (blogId) {
this.checkTagsFeature(blogId); this.checkTagsFeature(blogId);
} }
// Listen for blog selection changes
this.form this.form
.get('blogId') .get('blogId')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
@ -142,7 +149,7 @@ export class BlogPostFormComponent implements OnInit {
}) })
.subscribe(feature => { .subscribe(feature => {
const { isEnabled } = feature || {}; const { isEnabled } = feature || {};
this.isTagsEnabled = isEnabled; this.isTagsEnabled.set(isEnabled);
}); });
} }
@ -152,7 +159,7 @@ export class BlogPostFormComponent implements OnInit {
this.coverImageFile = input.files[0]; this.coverImageFile = input.files[0];
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e: any) => { reader.onload = (e: any) => {
this.coverImagePreview = e.target.result; this.coverImagePreview.set(e.target.result);
}; };
reader.readAsDataURL(this.coverImageFile); reader.readAsDataURL(this.coverImageFile);
} }
@ -160,7 +167,7 @@ export class BlogPostFormComponent implements OnInit {
removeCoverImage() { removeCoverImage() {
this.coverImageFile = null; this.coverImageFile = null;
this.coverImagePreview = null; this.coverImagePreview.set(null);
this.form.patchValue({ coverImageMediaId: null }); this.form.patchValue({ coverImageMediaId: null });
} }
@ -183,11 +190,12 @@ export class BlogPostFormComponent implements OnInit {
} }
private setTags(blogPostId: string) { private setTags(blogPostId: string) {
if (!this.tags || !this.tags.trim()) { const tagsValue = this.tags();
if (!tagsValue || !tagsValue.trim()) {
return of(null); return of(null);
} }
const tagArray = this.tags const tagArray = tagsValue
.split(',') .split(',')
.map(t => t.trim()) .map(t => t.trim())
.filter(t => t.length > 0); .filter(t => t.length > 0);
@ -204,7 +212,6 @@ export class BlogPostFormComponent implements OnInit {
} }
private executeSaveOperation(operation: 'save' | 'draft' | 'publish' | 'sendToReview') { private executeSaveOperation(operation: 'save' | 'draft' | 'publish' | 'sendToReview') {
// First upload cover image if selected
this.uploadCoverImage() this.uploadCoverImage()
.pipe( .pipe(
tap(coverImageMediaId => { tap(coverImageMediaId => {
@ -213,11 +220,12 @@ export class BlogPostFormComponent implements OnInit {
} }
}), }),
switchMap(() => { switchMap(() => {
if (this.isEditMode) { if (this.isEditMode()) {
if (!this.blogPost || !this.blogPostId) { const currentBlogPost = this.blogPost();
if (!currentBlogPost || !this.blogPostId) {
return of(null); return of(null);
} }
return this.blogPostFormService.update(this.blogPostId, this.form, this.blogPost); return this.blogPostFormService.update(this.blogPostId, this.form, currentBlogPost);
} }
switch (operation) { switch (operation) {
@ -236,7 +244,6 @@ export class BlogPostFormComponent implements OnInit {
if (!result || !result.id) { if (!result || !result.id) {
return of(null); return of(null);
} }
// Set tags after blog post is created/updated
return forkJoin([of(result), this.setTags(result.id)]); return forkJoin([of(result), this.setTags(result.id)]);
}), }),
) )

8
npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.html

@ -1,4 +1,4 @@
<abp-page [title]="'CmsKit::BlogPosts' | abpLocalization" [toolbar]="data.items"> <abp-page [title]="'CmsKit::BlogPosts' | abpLocalization" [toolbar]="data().items">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -35,6 +35,10 @@
</div> </div>
<div class="card"> <div class="card">
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> <abp-extensible-table
[data]="data().items"
[recordsTotal]="data().totalCount"
[list]="list"
/>
</div> </div>
</abp-page> </abp-page>

55
npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.ts

@ -1,4 +1,5 @@
import { Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core';
@ -14,6 +15,7 @@ import {
import { eCmsKitAdminComponents } from '../../../enums'; import { eCmsKitAdminComponents } from '../../../enums';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-post-list', selector: 'abp-blog-post-list',
templateUrl: './blog-post-list.component.html', templateUrl: './blog-post-list.component.html',
providers: [ providers: [
@ -25,21 +27,35 @@ import { eCmsKitAdminComponents } from '../../../enums';
], ],
imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule], imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule],
}) })
export class BlogPostListComponent implements OnInit { export class BlogPostListComponent {
data: PagedResultDto<BlogPostListDto> = { items: [], totalCount: 0 };
public readonly list = inject(ListService<BlogPostGetListInput>); public readonly list = inject(ListService<BlogPostGetListInput>);
private blogPostService = inject(BlogPostAdminService); private blogPostService = inject(BlogPostAdminService);
private confirmationService = inject(ConfirmationService); private confirmationService = inject(ConfirmationService);
readonly data = toSignal(
this.list.hookToQuery(query => {
let filters: Partial<BlogPostGetListInput> = {};
if (this.list.filter) {
filters.filter = this.list.filter;
}
if (this.statusFilter !== null) {
filters.status = this.statusFilter;
}
const input: BlogPostGetListInput = {
...query,
...filters,
};
return this.blogPostService.getList(input);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<BlogPostListDto>,
},
);
filter = ''; filter = '';
statusFilter: BlogPostStatus | null = null; statusFilter: BlogPostStatus | null = null;
BlogPostStatus = BlogPostStatus; BlogPostStatus = BlogPostStatus;
ngOnInit() {
this.hookToQuery();
}
onSearch() { onSearch() {
this.list.filter = this.filter; this.list.filter = this.filter;
this.list.get(); this.list.get();
@ -49,25 +65,6 @@ export class BlogPostListComponent implements OnInit {
this.list.get(); this.list.get();
} }
private hookToQuery() {
this.list
.hookToQuery(query => {
let filters: Partial<BlogPostGetListInput> = {};
if (this.list.filter) {
filters.filter = this.list.filter;
}
if (this.statusFilter !== null) {
filters.status = this.statusFilter;
}
const input: BlogPostGetListInput = {
...query,
...filters,
};
return this.blogPostService.getList(input);
})
.subscribe(res => (this.data = res));
}
delete(id: string, title: string) { delete(id: string, title: string) {
this.confirmationService this.confirmationService
.warn('CmsKit::BlogPostDeletionConfirmationMessage', 'AbpUi::AreYouSure', { .warn('CmsKit::BlogPostDeletionConfirmationMessage', 'AbpUi::AreYouSure', {
@ -77,9 +74,7 @@ export class BlogPostListComponent implements OnInit {
}) })
.subscribe((status: Confirmation.Status) => { .subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) { if (status === Confirmation.Status.confirm) {
this.blogPostService.delete(id).subscribe(() => { this.blogPostService.delete(id).subscribe(() => this.list.get());
this.list.get();
});
} }
}); });
} }

10
npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.html

@ -4,8 +4,8 @@
</ng-template> </ng-template>
<ng-template #abpBody> <ng-template #abpBody>
@if (form) { @if (form(); as currentForm) {
<form [formGroup]="form" (ngSubmit)="save()" [validateOnSubmit]="true"> <form [formGroup]="currentForm" (ngSubmit)="save()" [validateOnSubmit]="true">
<div formArrayName="features"> <div formArrayName="features">
@for (featureControl of featuresFormArray.controls; track $index; let i = $index) { @for (featureControl of featuresFormArray.controls; track $index; let i = $index) {
<div class="mb-3" [formGroupName]="i"> <div class="mb-3" [formGroupName]="i">
@ -19,7 +19,7 @@
formControlName="isEnabled" formControlName="isEnabled"
/> />
<label class="form-check-label" for="feature-{{ i }}"> <label class="form-check-label" for="feature-{{ i }}">
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }} {{ 'CmsKit::' + features()[i]?.featureName | abpLocalization }}
</label> </label>
</div> </div>
} @else { } @else {
@ -36,7 +36,7 @@
[disabled]="true" [disabled]="true"
/> />
<label class="form-check-label" for="feature-{{ i }}"> <label class="form-check-label" for="feature-{{ i }}">
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }} {{ 'CmsKit::' + features()[i]?.featureName | abpLocalization }}
</label> </label>
</div> </div>
</div> </div>
@ -58,7 +58,7 @@
<button type="button" class="btn btn-outline-primary" abpClose> <button type="button" class="btn btn-outline-primary" abpClose>
{{ 'AbpUi::Cancel' | abpLocalization }} {{ 'AbpUi::Cancel' | abpLocalization }}
</button> </button>
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()"> <abp-button iconClass="fa fa-check" [disabled]="form()?.invalid" (click)="save()">
{{ 'AbpUi::Save' | abpLocalization }} {{ 'AbpUi::Save' | abpLocalization }}
</abp-button> </abp-button>
</ng-template> </ng-template>

55
npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.ts

@ -1,4 +1,12 @@
import { Component, OnInit, inject, input, output } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
inject,
input,
OnInit,
output,
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormArray, FormBuilder, FormGroup } from '@angular/forms'; import { ReactiveFormsModule, FormArray, FormBuilder, FormGroup } from '@angular/forms';
import { NgxValidateCoreModule } from '@ngx-validate/core'; import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -22,6 +30,7 @@ export interface BlogFeaturesModalVisibleChange {
} }
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-features-modal', selector: 'abp-blog-features-modal',
templateUrl: './blog-features-modal.component.html', templateUrl: './blog-features-modal.component.html',
imports: [ imports: [
@ -42,8 +51,8 @@ export class BlogFeaturesModalComponent implements OnInit {
blogId = input<string>(); blogId = input<string>();
visibleChange = output<BlogFeaturesModalVisibleChange>(); visibleChange = output<BlogFeaturesModalVisibleChange>();
form: FormGroup; readonly form = signal<FormGroup | undefined>(undefined);
features: BlogFeatureDto[] = []; readonly features = signal<BlogFeatureDto[]>([]);
private initialFeatureStates: Map<string, boolean> = new Map(); private initialFeatureStates: Map<string, boolean> = new Map();
ngOnInit() { ngOnInit() {
@ -54,19 +63,19 @@ export class BlogFeaturesModalComponent implements OnInit {
private loadFeatures() { private loadFeatures() {
this.blogFeatureService.getList(this.blogId()!).subscribe(features => { this.blogFeatureService.getList(this.blogId()!).subscribe(features => {
this.features = features.sort((a, b) => const sorted = features.sort((a, b) =>
(a.featureName || '').localeCompare(b.featureName || ''), (a.featureName || '').localeCompare(b.featureName || ''),
); );
// Store initial states this.features.set(sorted);
this.initialFeatureStates = new Map( this.initialFeatureStates = new Map(
this.features.map(f => [f.featureName || '', f.isEnabled || false]), sorted.map(f => [f.featureName || '', f.isEnabled || false]),
); );
this.buildForm(); this.buildForm(sorted);
}); });
} }
private buildForm() { private buildForm(features: BlogFeatureDto[]) {
const featureControls = this.features.map(feature => const featureControls = features.map(feature =>
this.fb.group({ this.fb.group({
featureName: [feature.featureName], featureName: [feature.featureName],
isEnabled: [feature.isEnabled], isEnabled: [feature.isEnabled],
@ -74,13 +83,15 @@ export class BlogFeaturesModalComponent implements OnInit {
}), }),
); );
this.form = this.fb.group({ this.form.set(
features: this.fb.array(featureControls), this.fb.group({
}); features: this.fb.array(featureControls),
}),
);
} }
get featuresFormArray(): FormArray { get featuresFormArray(): FormArray {
return this.form.get('features') as FormArray; return this.form()!.get('features') as FormArray;
} }
onVisibleChange(visible: boolean, refresh = false) { onVisibleChange(visible: boolean, refresh = false) {
@ -88,20 +99,19 @@ export class BlogFeaturesModalComponent implements OnInit {
} }
save() { save() {
if (!this.form.valid || !this.blogId()) { const currentForm = this.form();
if (!currentForm?.valid || !this.blogId()) {
return; return;
} }
const featuresArray = this.form.get('features') as FormArray; const featuresArray = currentForm.get('features') as FormArray;
// Only save features that have changed
const changedFeatures: BlogFeatureInputDto[] = featuresArray.controls const changedFeatures: BlogFeatureInputDto[] = featuresArray.controls
.map(control => { .map(control => {
const featureName = control.get('featureName')?.value; const featureName = control.get('featureName')?.value;
const isEnabled = control.get('isEnabled')?.value; const isEnabled = control.get('isEnabled')?.value;
const initialIsEnabled = this.initialFeatureStates.get(featureName); const initialIsEnabled = this.initialFeatureStates.get(featureName);
// Only include if the value has changed
if (featureName && initialIsEnabled !== isEnabled) { if (featureName && initialIsEnabled !== isEnabled) {
return { return {
featureName, featureName,
@ -112,23 +122,18 @@ export class BlogFeaturesModalComponent implements OnInit {
}) })
.filter((input): input is BlogFeatureInputDto => input !== null); .filter((input): input is BlogFeatureInputDto => input !== null);
// If no features changed, just close the modal
if (changedFeatures.length === 0) { if (changedFeatures.length === 0) {
this.onVisibleChange(false, false); this.onVisibleChange(false, false);
return; return;
} }
// Save only changed features
const saveObservables = changedFeatures.map(input => const saveObservables = changedFeatures.map(input =>
this.blogFeatureService.set(this.blogId()!, input), this.blogFeatureService.set(this.blogId()!, input),
); );
// Use forkJoin to save all changed features at once forkJoin(saveObservables).subscribe(() => {
forkJoin(saveObservables).subscribe({ this.onVisibleChange(false, true);
next: () => { this.toasterService.success('AbpUi::SavedSuccessfully');
this.onVisibleChange(false, true);
this.toasterService.success('AbpUi::SavedSuccessfully');
},
}); });
} }
} }

12
npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.html

@ -1,15 +1,15 @@
<abp-page [title]="'CmsKit::Blogs' | abpLocalization" [toolbar]="data.items"> <abp-page [title]="'CmsKit::Blogs' | abpLocalization" [toolbar]="data().items">
<div class="card"> <div class="card">
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> <abp-extensible-table [data]="data().items" [recordsTotal]="data().totalCount" [list]="list" />
</div> </div>
@if (isModalVisible) { @if (isModalVisible()) {
<abp-blog-modal [selected]="selected" (visibleChange)="onVisibleModalChange($event)" /> <abp-blog-modal [selected]="selected()" (visibleChange)="onVisibleModalChange($event)" />
} }
@if (isFeaturesModalVisible) { @if (isFeaturesModalVisible()) {
<abp-blog-features-modal <abp-blog-features-modal
[blogId]="selectedBlogId" [blogId]="selectedBlogId()"
(visibleChange)="onFeaturesModalChange($event)" (visibleChange)="onFeaturesModalChange($event)"
/> />
} }

79
npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.ts

@ -1,4 +1,5 @@
import { Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core';
@ -14,6 +15,7 @@ import {
} from '../blog-features-modal/blog-features-modal.component'; } from '../blog-features-modal/blog-features-modal.component';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-list', selector: 'abp-blog-list',
templateUrl: './blog-list.component.html', templateUrl: './blog-list.component.html',
providers: [ providers: [
@ -33,22 +35,33 @@ import {
BlogFeaturesModalComponent, BlogFeaturesModalComponent,
], ],
}) })
export class BlogListComponent implements OnInit { export class BlogListComponent {
data: PagedResultDto<BlogDto> = { items: [], totalCount: 0 };
public readonly list = inject(ListService<BlogGetListInput>); public readonly list = inject(ListService<BlogGetListInput>);
private blogService = inject(BlogAdminService); private blogService = inject(BlogAdminService);
private confirmationService = inject(ConfirmationService); private confirmationService = inject(ConfirmationService);
filter = ''; readonly data = toSignal(
isModalVisible = false; this.list.hookToQuery(query => {
selected?: BlogDto; let filters: Partial<BlogGetListInput> = {};
isFeaturesModalVisible = false; if (this.list.filter) {
selectedBlogId?: string; filters.filter = this.list.filter;
}
const input: BlogGetListInput = {
...query,
...filters,
};
return this.blogService.getList(input);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<BlogDto>,
},
);
ngOnInit() { filter = '';
this.hookToQuery(); readonly isModalVisible = signal(false);
} readonly selected = signal<BlogDto | undefined>(undefined);
readonly isFeaturesModalVisible = signal(false);
readonly selectedBlogId = signal<string | undefined>(undefined);
onSearch() { onSearch() {
this.list.filter = this.filter; this.list.filter = this.filter;
@ -56,14 +69,14 @@ export class BlogListComponent implements OnInit {
} }
add() { add() {
this.selected = {} as BlogDto; this.selected.set({} as BlogDto);
this.isModalVisible = true; this.isModalVisible.set(true);
} }
edit(id: string) { edit(id: string) {
this.blogService.get(id).subscribe(blog => { this.blogService.get(id).subscribe(blog => {
this.selected = blog; this.selected.set(blog);
this.isModalVisible = true; this.isModalVisible.set(true);
}); });
} }
@ -74,34 +87,14 @@ export class BlogListComponent implements OnInit {
}) })
.subscribe((status: Confirmation.Status) => { .subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) { if (status === Confirmation.Status.confirm) {
this.blogService.delete(id).subscribe(() => { this.blogService.delete(id).subscribe(() => this.list.get());
this.list.get();
});
} }
}); });
} }
openFeatures(id: string) { openFeatures(id: string) {
this.selectedBlogId = id; this.selectedBlogId.set(id);
this.isFeaturesModalVisible = true; this.isFeaturesModalVisible.set(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) { onVisibleModalChange(visibilityChange: BlogModalVisibleChange) {
@ -111,8 +104,8 @@ export class BlogListComponent implements OnInit {
if (visibilityChange.refresh) { if (visibilityChange.refresh) {
this.list.get(); this.list.get();
} }
this.selected = null; this.selected.set(undefined);
this.isModalVisible = false; this.isModalVisible.set(false);
} }
onFeaturesModalChange(visibilityChange: BlogFeaturesModalVisibleChange) { onFeaturesModalChange(visibilityChange: BlogFeaturesModalVisibleChange) {
@ -122,7 +115,7 @@ export class BlogListComponent implements OnInit {
if (visibilityChange.refresh) { if (visibilityChange.refresh) {
this.list.get(); this.list.get();
} }
this.selectedBlogId = null; this.selectedBlogId.set(undefined);
this.isFeaturesModalVisible = false; this.isFeaturesModalVisible.set(false);
} }
} }

2
npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.html

@ -1,4 +1,4 @@
<abp-modal [visible]="true" (visibleChange)="onVisibleChange($event)" [busy]="modalBusy"> <abp-modal [visible]="true" (visibleChange)="onVisibleChange($event)" [busy]="modalBusy()">
<ng-template #abpHeader> <ng-template #abpHeader>
<h3> <h3>
{{ (selected()?.id ? 'AbpUi::Edit' : 'AbpUi::New') | abpLocalization }} {{ (selected()?.id ? 'AbpUi::Edit' : 'AbpUi::New') | abpLocalization }}

22
npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.ts

@ -1,4 +1,14 @@
import { Component, OnInit, inject, Injector, input, output, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
Injector,
input,
OnInit,
output,
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormGroup } from '@angular/forms'; import { ReactiveFormsModule, FormGroup } from '@angular/forms';
import { NgxValidateCoreModule } from '@ngx-validate/core'; import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -24,6 +34,7 @@ export interface BlogModalVisibleChange {
} }
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-modal', selector: 'abp-blog-modal',
templateUrl: './blog-modal.component.html', templateUrl: './blog-modal.component.html',
imports: [ imports: [
@ -46,7 +57,7 @@ export class BlogModalComponent implements OnInit {
selected = input<BlogDto>(); selected = input<BlogDto>();
visibleChange = output<BlogModalVisibleChange>(); visibleChange = output<BlogModalVisibleChange>();
modalBusy = false; readonly modalBusy = signal(false);
form: FormGroup; form: FormGroup;
@ -65,12 +76,13 @@ export class BlogModalComponent implements OnInit {
} }
save() { save() {
if (this.modalBusy) { if (this.modalBusy()) {
return; return;
} }
this.modalBusy = true; this.modalBusy.set(true);
if (!this.form.valid) { if (!this.form.valid) {
this.modalBusy.set(false);
return; return;
} }
@ -86,7 +98,7 @@ export class BlogModalComponent implements OnInit {
} as UpdateBlogDto); } as UpdateBlogDto);
} }
observable$.pipe(finalize(() => (this.modalBusy = false))).subscribe(() => { observable$.pipe(finalize(() => this.modalBusy.set(false))).subscribe(() => {
this.onVisibleChange(false, true); this.onVisibleChange(false, true);
this.toasterService.success('AbpUi::SavedSuccessfully'); this.toasterService.success('AbpUi::SavedSuccessfully');
}); });

2
npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.html

@ -28,7 +28,7 @@
</div> </div>
<hr class="my-3" /> <hr class="my-3" />
<div> <div>
<abp-button (click)="submit()" iconClass="fa fa-save" [loading]="loading"> <abp-button (click)="submit()" iconClass="fa fa-save" [loading]="loading()">
{{ 'AbpAccount::Save' | abpLocalization }} {{ 'AbpAccount::Save' | abpLocalization }}
</abp-button> </abp-button>
</div> </div>

10
npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.ts

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { import {
NgbNav, NgbNav,
@ -33,12 +33,12 @@ import { CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT } from '../comments';
ReactiveFormsModule, ReactiveFormsModule,
], ],
}) })
export class CmsSettingsComponent { export class CmsSettingsComponent implements OnInit {
readonly commentAdminService = inject(CommentAdminService); readonly commentAdminService = inject(CommentAdminService);
readonly configState = inject(ConfigStateService); readonly configState = inject(ConfigStateService);
readonly toaster = inject(ToasterService); readonly toaster = inject(ToasterService);
loading = false; readonly loading = signal(false);
commentApprovalControl = new FormControl(false); commentApprovalControl = new FormControl(false);
@ -49,12 +49,12 @@ export class CmsSettingsComponent {
} }
submit() { submit() {
this.loading = true; this.loading.set(true);
this.commentAdminService this.commentAdminService
.updateSettings({ commentRequireApprovement: this.commentApprovalControl.value }) .updateSettings({ commentRequireApprovement: this.commentApprovalControl.value })
.pipe( .pipe(
finalize(() => { finalize(() => {
this.loading = false; this.loading.set(false);
this.configState.refreshAppState().subscribe(); this.configState.refreshAppState().subscribe();
}), }),
) )

6
npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.html

@ -1,5 +1,5 @@
<abp-page [title]="'CmsKit::Comments' | abpLocalization"> <abp-page [title]="'CmsKit::Comments' | abpLocalization">
@if (comment) { @if (comment(); as comment) {
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<table class="table"> <table class="table">
@ -59,7 +59,7 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<form [formGroup]="filterForm" (ngSubmit)="onFilter()"> <form [formGroup]="filterForm" (ngSubmit)="onFilter()">
<input type="hidden" [value]="commentId" /> <input type="hidden" [value]="commentId()" />
<div class="row"> <div class="row">
<div class="col-lg-4 col-md-12"> <div class="col-lg-4 col-md-12">
<div class="row"> <div class="row">
@ -142,6 +142,6 @@
<h3>{{ 'CmsKit::RepliesToThisComment' | abpLocalization }}</h3> <h3>{{ 'CmsKit::RepliesToThisComment' | abpLocalization }}</h3>
<div class="card"> <div class="card">
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> <abp-extensible-table [data]="data().items" [recordsTotal]="data().totalCount" [list]="list" />
</div> </div>
</abp-page> </abp-page>

46
npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.ts

@ -1,4 +1,5 @@
import { Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CommonModule, DatePipe } from '@angular/common'; import { CommonModule, DatePipe } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -18,6 +19,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { CommentEntityService } from '../../../services'; import { CommentEntityService } from '../../../services';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-comment-details', selector: 'abp-comment-details',
templateUrl: './comment-details.component.html', templateUrl: './comment-details.component.html',
providers: [ providers: [
@ -46,8 +48,8 @@ import { CommentEntityService } from '../../../services';
], ],
}) })
export class CommentDetailsComponent implements OnInit { export class CommentDetailsComponent implements OnInit {
comment: CommentWithAuthorDto | null = null; readonly comment = signal<CommentWithAuthorDto | null>(null);
data: PagedResultDto<CommentWithAuthorDto> = { items: [], totalCount: 0 }; readonly commentId = signal<string | null>(null);
readonly list = inject(ListService<CommentGetListInput>); readonly list = inject(ListService<CommentGetListInput>);
readonly commentEntityService = inject(CommentEntityService); readonly commentEntityService = inject(CommentEntityService);
@ -57,19 +59,33 @@ export class CommentDetailsComponent implements OnInit {
private router = inject(Router); private router = inject(Router);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
readonly data = toSignal(
this.list.hookToQuery(query => {
const filters = (this.list.filter as Partial<CommentGetListInput>) || {};
const input: CommentGetListInput = {
repliedCommentId: this.commentId() ?? undefined,
...query,
...filters,
};
return this.commentService.getList(input);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<CommentWithAuthorDto>,
},
);
filterForm!: FormGroup; filterForm!: FormGroup;
commentApproveStateOptions = commentApproveStateOptions; commentApproveStateOptions = commentApproveStateOptions;
commentId!: string;
requireApprovement: boolean; requireApprovement: boolean;
ngOnInit() { ngOnInit() {
this.route.params.subscribe(params => { this.route.params.subscribe(params => {
const id = params['id']; const id = params['id'];
if (id) { if (id) {
this.commentId = id; this.commentId.set(id);
this.loadComment(id); this.loadComment(id);
this.createFilterForm(); this.createFilterForm();
this.hookToQuery(); this.list.get();
} }
}); });
this.requireApprovement = this.commentEntityService.requireApprovement; this.requireApprovement = this.commentEntityService.requireApprovement;
@ -86,7 +102,7 @@ export class CommentDetailsComponent implements OnInit {
private loadComment(id: string) { private loadComment(id: string) {
this.commentService.get(id).subscribe(comment => { this.commentService.get(id).subscribe(comment => {
this.comment = comment; this.comment.set(comment);
}); });
} }
@ -95,7 +111,7 @@ export class CommentDetailsComponent implements OnInit {
const filters: Partial<CommentGetListInput> = { const filters: Partial<CommentGetListInput> = {
author: formValue.author || undefined, author: formValue.author || undefined,
commentApproveState: formValue.commentApproveState, commentApproveState: formValue.commentApproveState,
repliedCommentId: this.commentId, repliedCommentId: this.commentId() ?? undefined,
creationStartDate: formValue.creationStartDate || undefined, creationStartDate: formValue.creationStartDate || undefined,
creationEndDate: formValue.creationEndDate || undefined, creationEndDate: formValue.creationEndDate || undefined,
}; };
@ -104,20 +120,6 @@ export class CommentDetailsComponent implements OnInit {
this.list.get(); this.list.get();
} }
private hookToQuery() {
this.list
.hookToQuery(query => {
const filters = (this.list.filter as Partial<CommentGetListInput>) || {};
const input: CommentGetListInput = {
repliedCommentId: this.commentId,
...query,
...filters,
};
return this.commentService.getList(input);
})
.subscribe(res => (this.data = res));
}
navigateToReply(id: string) { navigateToReply(id: string) {
this.router.navigate(['/cms/comments', id]); this.router.navigate(['/cms/comments', id]);
} }

2
npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.html

@ -87,6 +87,6 @@
</div> </div>
<div class="card"> <div class="card">
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> <abp-extensible-table [data]="data().items" [recordsTotal]="data().totalCount" [list]="list" />
</div> </div>
</abp-page> </abp-page>

34
npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.ts

@ -1,4 +1,5 @@
import { Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { NgbDateAdapter, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateAdapter, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
@ -17,6 +18,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { CommentEntityService } from '../../../services'; import { CommentEntityService } from '../../../services';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-comment-list', selector: 'abp-comment-list',
templateUrl: './comment-list.component.html', templateUrl: './comment-list.component.html',
providers: [ providers: [
@ -44,21 +46,32 @@ import { CommentEntityService } from '../../../services';
], ],
}) })
export class CommentListComponent implements OnInit { export class CommentListComponent implements OnInit {
data: PagedResultDto<CommentWithAuthorDto> = { items: [], totalCount: 0 };
readonly list = inject(ListService<CommentGetListInput>); readonly list = inject(ListService<CommentGetListInput>);
readonly commentEntityService = inject(CommentEntityService); readonly commentEntityService = inject(CommentEntityService);
private commentService = inject(CommentAdminService); private commentService = inject(CommentAdminService);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
readonly data = toSignal(
this.list.hookToQuery(query => {
const filters = (this.list.filter as Partial<CommentGetListInput>) || {};
const input: CommentGetListInput = {
...query,
...filters,
};
return this.commentService.getList(input);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<CommentWithAuthorDto>,
},
);
filterForm!: FormGroup; filterForm!: FormGroup;
commentApproveStateOptions = commentApproveStateOptions; commentApproveStateOptions = commentApproveStateOptions;
requireApprovement: boolean; requireApprovement: boolean;
ngOnInit() { ngOnInit() {
this.createFilterForm(); this.createFilterForm();
this.hookToQuery();
this.requireApprovement = this.commentEntityService.requireApprovement; this.requireApprovement = this.commentEntityService.requireApprovement;
} }
@ -85,17 +98,4 @@ export class CommentListComponent implements OnInit {
this.list.filter = filters as any; this.list.filter = filters as any;
this.list.get(); this.list.get();
} }
private hookToQuery() {
this.list
.hookToQuery(query => {
const filters = (this.list.filter as Partial<CommentGetListInput>) || {};
const input: CommentGetListInput = {
...query,
...filters,
};
return this.commentService.getList(input);
})
.subscribe(res => (this.data = res));
}
} }

2
npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.html

@ -6,7 +6,7 @@
ngbNav ngbNav
#nav="ngbNav" #nav="ngbNav"
class="nav-tabs" class="nav-tabs"
[(activeId)]="activeTab" [activeId]="activeTab()"
(activeIdChange)="onTabChange($event)" (activeIdChange)="onTabChange($event)"
> >
<li ngbNavItem="script"> <li ngbNavItem="script">

14
npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.ts

@ -1,4 +1,11 @@
import { Component, OnInit, inject, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
OnInit,
signal,
} from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms'; import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -15,6 +22,7 @@ import {
} from '@abp/ng.cms-kit/proxy'; } from '@abp/ng.cms-kit/proxy';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-global-resources', selector: 'abp-global-resources',
imports: [ imports: [
CommonModule, CommonModule,
@ -34,7 +42,7 @@ export class GlobalResourcesComponent implements OnInit {
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
form: FormGroup; form: FormGroup;
activeTab: string = 'script'; readonly activeTab = signal('script');
ngOnInit() { ngOnInit() {
this.buildForm(); this.buildForm();
@ -66,7 +74,7 @@ export class GlobalResourcesComponent implements OnInit {
} }
onTabChange(activeId: string) { onTabChange(activeId: string) {
this.activeTab = activeId; this.activeTab.set(activeId);
} }
save() { save() {

18
npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-list/menu-item-list.component.html

@ -1,12 +1,12 @@
<abp-page [title]="'CmsKit::MenuItems' | abpLocalization" [toolbar]="nodes"> <abp-page [title]="'CmsKit::MenuItems' | abpLocalization" [toolbar]="nodes()">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@if (nodes.length > 0) { @if (nodes().length > 0) {
<abp-tree <abp-tree
[nodes]="nodes" [nodes]="nodes()"
[draggable]="draggable" [draggable]="draggable()"
[expandedKeys]="expandedKeys" [expandedKeys]="expandedKeys()"
[selectedNode]="selectedNode" [selectedNode]="selectedNode()"
(selectedNodeChange)="onSelectedNodeChange($event)" (selectedNodeChange)="onSelectedNodeChange($event)"
[beforeDrop]="beforeDrop" [beforeDrop]="beforeDrop"
(dropOver)="onDrop($event)" (dropOver)="onDrop($event)"
@ -47,10 +47,10 @@
</div> </div>
</abp-page> </abp-page>
@if (isModalVisible) { @if (isModalVisible()) {
<abp-menu-item-modal <abp-menu-item-modal
[selected]="selectedMenuItem || undefined" [selected]="selectedMenuItem() || undefined"
[parentId]="parentId" [parentId]="parentId()"
(visibleChange)="onVisibleModalChange($event)" (visibleChange)="onVisibleModalChange($event)"
/> />
} }

69
npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-list/menu-item-list.component.ts

@ -1,4 +1,4 @@
import { Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { PageComponent } from '@abp/ng.components/page'; import { PageComponent } from '@abp/ng.components/page';
@ -19,6 +19,7 @@ import {
} from '../menu-item-modal/menu-item-modal.component'; } from '../menu-item-modal/menu-item-modal.component';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-menu-item-list', selector: 'abp-menu-item-list',
templateUrl: './menu-item-list.component.html', templateUrl: './menu-item-list.component.html',
imports: [ imports: [
@ -41,13 +42,13 @@ export class MenuItemListComponent implements OnInit {
private menuItemService = inject(MenuItemAdminService); private menuItemService = inject(MenuItemAdminService);
private confirmationService = inject(ConfirmationService); private confirmationService = inject(ConfirmationService);
nodes: any[] = []; readonly nodes = signal<any[]>([]);
selectedNode: MenuItemDto | null = null; readonly selectedNode = signal<MenuItemDto | null>(null);
expandedKeys: string[] = []; readonly expandedKeys = signal<string[]>([]);
draggable = true; readonly draggable = signal(true);
isModalVisible = false; readonly isModalVisible = signal(false);
selectedMenuItem: MenuItemDto | MenuItemWithDetailsDto | null = null; readonly selectedMenuItem = signal<MenuItemDto | MenuItemWithDetailsDto | null>(null);
parentId: string | null = null; readonly parentId = signal<string | null>(null);
ngOnInit() { ngOnInit() {
this.loadMenuItems(); this.loadMenuItems();
@ -56,11 +57,12 @@ export class MenuItemListComponent implements OnInit {
private loadMenuItems() { private loadMenuItems() {
this.menuItemService.getList().subscribe(result => { this.menuItemService.getList().subscribe(result => {
if (result.items && result.items.length > 0) { if (result.items && result.items.length > 0) {
this.nodes = this.buildTreeNodes(result.items); const treeNodes = this.buildTreeNodes(result.items);
// Expand all nodes by default this.nodes.set(treeNodes);
this.expandedKeys = this.nodes.map(n => n.key); this.expandedKeys.set(treeNodes.map(n => n.key));
} else { } else {
this.nodes = []; this.nodes.set([]);
this.expandedKeys.set([]);
} }
}); });
} }
@ -69,7 +71,6 @@ export class MenuItemListComponent implements OnInit {
const nodeMap = new Map<string, any>(); const nodeMap = new Map<string, any>();
const rootNodes: any[] = []; const rootNodes: any[] = [];
// First pass: create all nodes
items.forEach(item => { items.forEach(item => {
const node: any = { const node: any = {
key: item.id, key: item.id,
@ -81,7 +82,6 @@ export class MenuItemListComponent implements OnInit {
nodeMap.set(item.id!, node); nodeMap.set(item.id!, node);
}); });
// Second pass: build tree structure
items.forEach(item => { items.forEach(item => {
const node = nodeMap.get(item.id!); const node = nodeMap.get(item.id!);
if (item.parentId) { if (item.parentId) {
@ -97,7 +97,6 @@ export class MenuItemListComponent implements OnInit {
} }
}); });
// Sort by order
const sortByOrder = (nodes: any[]) => { const sortByOrder = (nodes: any[]) => {
nodes.sort((a, b) => (a.entity.order || 0) - (b.entity.order || 0)); nodes.sort((a, b) => (a.entity.order || 0) - (b.entity.order || 0));
nodes.forEach(node => { nodes.forEach(node => {
@ -112,7 +111,7 @@ export class MenuItemListComponent implements OnInit {
} }
onSelectedNodeChange(node: any) { onSelectedNodeChange(node: any) {
this.selectedNode = node?.entity || null; this.selectedNode.set(node?.entity || null);
} }
onDrop(event: any) { onDrop(event: any) {
@ -143,16 +142,10 @@ export class MenuItemListComponent implements OnInit {
}; };
this.menuItemService.moveMenuItem(node.id!, input).subscribe({ this.menuItemService.moveMenuItem(node.id!, input).subscribe({
next: () => { next: () => this.loadMenuItems(),
this.loadMenuItems(); error: () => this.loadMenuItems(),
},
error: () => {
// Reload to rollback
this.loadMenuItems();
},
}); });
} else { } else {
// Reload to rollback
this.loadMenuItems(); this.loadMenuItems();
} }
}); });
@ -163,22 +156,22 @@ export class MenuItemListComponent implements OnInit {
}; };
add() { add() {
this.selectedMenuItem = null; this.selectedMenuItem.set(null);
this.parentId = null; this.parentId.set(null);
this.isModalVisible = true; this.isModalVisible.set(true);
} }
addSubMenuItem(parentId?: string) { addSubMenuItem(parentId?: string) {
this.selectedMenuItem = null; this.selectedMenuItem.set(null);
this.parentId = parentId || null; this.parentId.set(parentId || null);
this.isModalVisible = true; this.isModalVisible.set(true);
} }
edit(id: string) { edit(id: string) {
this.menuItemService.get(id).subscribe(menuItem => { this.menuItemService.get(id).subscribe(menuItem => {
this.selectedMenuItem = menuItem; this.selectedMenuItem.set(menuItem);
this.parentId = null; this.parentId.set(null);
this.isModalVisible = true; this.isModalVisible.set(true);
}); });
} }
@ -189,9 +182,9 @@ export class MenuItemListComponent implements OnInit {
if (visibilityChange.refresh) { if (visibilityChange.refresh) {
this.loadMenuItems(); this.loadMenuItems();
} }
this.selectedMenuItem = null; this.selectedMenuItem.set(null);
this.parentId = null; this.parentId.set(null);
this.isModalVisible = false; this.isModalVisible.set(false);
} }
delete(id: string, displayName?: string) { delete(id: string, displayName?: string) {
@ -204,9 +197,7 @@ export class MenuItemListComponent implements OnInit {
.subscribe((status: Confirmation.Status) => { .subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) { if (status === Confirmation.Status.confirm) {
this.menuItemService.delete(id).subscribe({ this.menuItemService.delete(id).subscribe({
next: () => { next: () => this.loadMenuItems(),
this.loadMenuItems();
},
}); });
} }
}); });

20
npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-modal/menu-item-modal.component.html

@ -16,7 +16,7 @@
ngbNav ngbNav
#nav="ngbNav" #nav="ngbNav"
class="nav-tabs" class="nav-tabs"
[(activeId)]="activeTab" [activeId]="activeTab()"
(activeIdChange)="onTabChange($event)" (activeIdChange)="onTabChange($event)"
> >
<li ngbNavItem="url"> <li ngbNavItem="url">
@ -51,9 +51,9 @@
type="button" type="button"
id="pageSelectDropdown" id="pageSelectDropdown"
ngbDropdownToggle ngbDropdownToggle
[class.text-muted]="!selectedPage" [class.text-muted]="!selectedPage()"
> >
<span>{{ selectedPage?.title || ('CmsKit::Page' | abpLocalization) }}</span> <span>{{ selectedPage()?.title || ('CmsKit::Page' | abpLocalization) }}</span>
</button> </button>
<div <div
class="dropdown-menu w-100" class="dropdown-menu w-100"
@ -65,16 +65,16 @@
<input <input
type="text" type="text"
class="form-control form-control-sm" class="form-control form-control-sm"
[(ngModel)]="pageSearchText" [ngModel]="pageSearchText()"
(ngModelChange)="onPageSearchChange($event)"
[ngModelOptions]="{ standalone: true }" [ngModelOptions]="{ standalone: true }"
(input)="onPageSearchChange($any($event.target).value)"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
placeholder="{{ 'AbpUi::Search' | abpLocalization }}" placeholder="{{ 'AbpUi::Search' | abpLocalization }}"
autocomplete="off" autocomplete="off"
/> />
</div> </div>
@if (filteredPages.length > 0) { @if (filteredPages().length > 0) {
@for (page of filteredPages; track page.id) { @for (page of filteredPages(); track page.id) {
<button <button
type="button" type="button"
class="dropdown-item" class="dropdown-item"
@ -83,16 +83,16 @@
{{ page.title }} {{ page.title }}
</button> </button>
} }
} @else if (pageSearchText && pageSearchText.length > 0) { } @else if (pageSearchText() && pageSearchText().length > 0) {
<div class="dropdown-item text-muted"> <div class="dropdown-item text-muted">
{{ 'No results found' }} {{ 'No results found' }}
</div> </div>
} @else if (pages.length === 0) { } @else if (pages().length === 0) {
<div class="dropdown-item text-muted"> <div class="dropdown-item text-muted">
{{ 'No pages found' }} {{ 'No pages found' }}
</div> </div>
} }
@if (selectedPage) { @if (selectedPage()) {
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<button <button
type="button" type="button"

85
npm/ng-packs/packages/cms-kit/admin/src/components/menus/menu-item-modal/menu-item-modal.component.ts

@ -1,4 +1,14 @@
import { Component, OnInit, inject, Injector, input, output, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
Injector,
input,
OnInit,
output,
signal,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormGroup, FormControl, FormsModule } from '@angular/forms'; import { ReactiveFormsModule, FormGroup, FormControl, FormsModule } from '@angular/forms';
@ -43,6 +53,7 @@ const TABS = {
} as const; } as const;
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-menu-item-modal', selector: 'abp-menu-item-modal',
templateUrl: './menu-item-modal.component.html', templateUrl: './menu-item-modal.component.html',
imports: [ imports: [
@ -81,13 +92,13 @@ export class MenuItemModalComponent implements OnInit {
// Form state // Form state
form: FormGroup; form: FormGroup;
activeTab: string = TABS.URL; readonly activeTab = signal<string>(TABS.URL);
// Page selection state // Page search state
pages: PageLookupDto[] = []; readonly pages = signal<PageLookupDto[]>([]);
selectedPage: PageLookupDto | null = null; readonly selectedPage = signal<PageLookupDto | null>(null);
pageSearchText: string = ''; readonly pageSearchText = signal('');
filteredPages: PageLookupDto[] = []; readonly filteredPages = signal<PageLookupDto[]>([]);
// Search subject for debouncing // Search subject for debouncing
private readonly pageSearchSubject = new Subject<string>(); private readonly pageSearchSubject = new Subject<string>();
@ -123,10 +134,10 @@ export class MenuItemModalComponent implements OnInit {
) )
.subscribe({ .subscribe({
next: result => { next: result => {
this.filteredPages = result.items || []; this.filteredPages.set(result.items || []);
}, },
error: () => { error: () => {
this.filteredPages = []; this.filteredPages.set([]);
}, },
}); });
} }
@ -156,8 +167,8 @@ export class MenuItemModalComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: ({ menuItem, pages }) => { next: ({ menuItem, pages }) => {
this.pages = pages.items || []; this.pages.set(pages.items || []);
this.filteredPages = this.pages; this.filteredPages.set(pages.items || []);
this.buildForm(menuItem); this.buildForm(menuItem);
this.initializePageSelection(menuItem); this.initializePageSelection(menuItem);
}, },
@ -176,8 +187,8 @@ export class MenuItemModalComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: result => { next: result => {
this.pages = result.items || []; this.pages.set(result.items || []);
this.filteredPages = this.pages; this.filteredPages.set(result.items || []);
}, },
error: () => { error: () => {
this.toasterService.error('AbpUi::ErrorMessage'); this.toasterService.error('AbpUi::ErrorMessage');
@ -190,16 +201,15 @@ export class MenuItemModalComponent implements OnInit {
*/ */
private initializePageSelection(menuItem: MenuItemWithDetailsDto | MenuItemDto): void { private initializePageSelection(menuItem: MenuItemWithDetailsDto | MenuItemDto): void {
if (menuItem.pageId) { if (menuItem.pageId) {
this.activeTab = TABS.PAGE; this.activeTab.set(TABS.PAGE);
this.selectedPage = this.pages.find(p => p.id === menuItem.pageId) || null; const page = this.pages().find(p => p.id === menuItem.pageId) || null;
this.selectedPage.set(page);
const url = this.selectedPage const url = page ? this.generateUrlFromPage(page) : menuItem.url || '';
? this.generateUrlFromPage(this.selectedPage)
: menuItem.url || '';
this.form.patchValue({ pageId: menuItem.pageId, url }, { emitEvent: false }); this.form.patchValue({ pageId: menuItem.pageId, url }, { emitEvent: false });
this.pageSearchText = this.selectedPage?.title || ''; this.pageSearchText.set(page?.title || '');
} else if (menuItem.url) { } else if (menuItem.url) {
this.activeTab = TABS.URL; this.activeTab.set(TABS.URL);
this.form.patchValue({ url: menuItem.url, pageId: null }, { emitEvent: false }); this.form.patchValue({ url: menuItem.url, pageId: null }, { emitEvent: false });
} }
} }
@ -220,9 +230,9 @@ export class MenuItemModalComponent implements OnInit {
* Handles page search input changes * Handles page search input changes
*/ */
onPageSearchChange(searchText: string): void { onPageSearchChange(searchText: string): void {
this.pageSearchText = searchText; this.pageSearchText.set(searchText);
if (!searchText?.trim()) { if (!searchText?.trim()) {
this.filteredPages = this.pages; this.filteredPages.set(this.pages());
return; return;
} }
this.pageSearchSubject.next(searchText); this.pageSearchSubject.next(searchText);
@ -232,8 +242,8 @@ export class MenuItemModalComponent implements OnInit {
* Handles dropdown open event * Handles dropdown open event
*/ */
onDropdownOpen(): void { onDropdownOpen(): void {
if (!this.pageSearchText?.trim()) { if (!this.pageSearchText()?.trim()) {
this.filteredPages = this.pages; this.filteredPages.set(this.pages());
} }
} }
@ -243,11 +253,11 @@ export class MenuItemModalComponent implements OnInit {
selectPage(page: PageLookupDto): void { selectPage(page: PageLookupDto): void {
if (!page) return; if (!page) return;
this.selectedPage = page; this.selectedPage.set(page);
const url = this.generateUrlFromPage(page); const url = this.generateUrlFromPage(page);
this.form.patchValue({ pageId: page.id, url }, { emitEvent: false }); this.form.patchValue({ pageId: page.id, url }, { emitEvent: false });
this.pageSearchText = page.title || ''; this.pageSearchText.set(page.title || '');
} }
/** /**
@ -255,9 +265,9 @@ export class MenuItemModalComponent implements OnInit {
*/ */
clearPageSelection(): void { clearPageSelection(): void {
this.form.patchValue({ pageId: null }, { emitEvent: false }); this.form.patchValue({ pageId: null }, { emitEvent: false });
this.selectedPage = null; this.selectedPage.set(null);
this.pageSearchText = ''; this.pageSearchText.set('');
this.filteredPages = this.pages; this.filteredPages.set(this.pages());
} }
/** /**
@ -282,7 +292,7 @@ export class MenuItemModalComponent implements OnInit {
* Loads the available menu order for new menu items * Loads the available menu order for new menu items
*/ */
private loadAvailableOrder(parentId: string | null, menuItemId?: string): void { private loadAvailableOrder(parentId: string | null, menuItemId?: string): void {
if (menuItemId) return; // Only needed for new items if (menuItemId) return;
const order$ = parentId const order$ = parentId
? this.menuItemService.getAvailableMenuOrder(parentId) ? this.menuItemService.getAvailableMenuOrder(parentId)
@ -306,7 +316,7 @@ export class MenuItemModalComponent implements OnInit {
* Handles tab changes * Handles tab changes
*/ */
onTabChange(activeId: string): void { onTabChange(activeId: string): void {
this.activeTab = activeId; this.activeTab.set(activeId);
} }
/** /**
@ -316,8 +326,8 @@ export class MenuItemModalComponent implements OnInit {
const urlValue = this.form.get('url')?.value; const urlValue = this.form.get('url')?.value;
if (urlValue && this.form.get('pageId')?.value) { if (urlValue && this.form.get('pageId')?.value) {
this.clearPageSelection(); this.clearPageSelection();
if (this.activeTab === TABS.PAGE) { if (this.activeTab() === TABS.PAGE) {
this.activeTab = TABS.URL; this.activeTab.set(TABS.URL);
} }
} }
} }
@ -356,17 +366,14 @@ export class MenuItemModalComponent implements OnInit {
const formValue = { ...this.form.value }; const formValue = { ...this.form.value };
if (formValue.pageId) { if (formValue.pageId) {
// If pageId is set, generate URL from the page const page = this.pages().find(p => p.id === formValue.pageId);
const selectedPage = this.pages.find(p => p.id === formValue.pageId); if (page) {
if (selectedPage) { formValue.url = this.generateUrlFromPage(page);
formValue.url = this.generateUrlFromPage(selectedPage);
} }
} else if (formValue.url) { } else if (formValue.url) {
// If URL is manually entered, ensure pageId is cleared
formValue.pageId = null; formValue.pageId = null;
} }
// Clean up undefined values
return { return {
...formValue, ...formValue,
url: formValue.url || undefined, url: formValue.url || undefined,

4
npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-form/page-form.component.html

@ -1,10 +1,10 @@
<abp-page [title]="'CmsKit::Pages' | abpLocalization"> <abp-page [title]="'CmsKit::Pages' | abpLocalization">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@if (form && (!isEditMode || page)) { @if (form && (!isEditMode() || page())) {
<form [formGroup]="form" (ngSubmit)="save()" [validateOnSubmit]="true"> <form [formGroup]="form" (ngSubmit)="save()" [validateOnSubmit]="true">
<!-- Basic fields outside tabs --> <!-- Basic fields outside tabs -->
<abp-extensible-form [selectedRecord]="page || {}" /> <abp-extensible-form [selectedRecord]="page() || {}" />
<!-- Tabs for Content, Script, and Style --> <!-- Tabs for Content, Script, and Style -->
<ul ngbNav #nav="ngbNav" class="nav-tabs mt-3"> <ul ngbNav #nav="ngbNav" class="nav-tabs mt-3">

41
npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-form/page-form.component.ts

@ -1,4 +1,12 @@
import { Component, OnInit, inject, Injector, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
Injector,
OnInit,
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms'; import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@ -23,6 +31,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { PageFormService } from '../../../services'; import { PageFormService } from '../../../services';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-page-form', selector: 'abp-page-form',
templateUrl: './page-form.component.html', templateUrl: './page-form.component.html',
providers: [ providers: [
@ -52,56 +61,58 @@ export class PageFormComponent implements OnInit {
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
form: FormGroup; form: FormGroup;
page: PageDto | null = null; readonly page = signal<PageDto | null>(null);
pageId: string | null = null; pageId: string | null = null;
isEditMode = false; readonly isEditMode = signal(false);
ngOnInit() { ngOnInit() {
const id = this.route.snapshot.params['id']; const id = this.route.snapshot.params['id'];
if (id) { if (id) {
this.isEditMode = true; this.isEditMode.set(true);
this.pageId = id; this.pageId = id;
this.loadPage(id); this.loadPage(id);
} else { } else {
this.isEditMode = false; this.isEditMode.set(false);
this.buildForm(); this.buildForm();
} }
} }
private loadPage(id: string) { private loadPage(id: string) {
this.pageService.get(id).subscribe(page => { this.pageService.get(id).subscribe(page => {
this.page = page; this.page.set(page);
this.buildForm(); this.buildForm();
}); });
} }
private buildForm() { private buildForm() {
const data = new FormPropData(this.injector, this.page || {}); const currentPage = this.page();
const data = new FormPropData(this.injector, currentPage || {});
const baseForm = generateFormFromProps(data); const baseForm = generateFormFromProps(data);
this.form = new FormGroup({ this.form = new FormGroup({
...baseForm.controls, ...baseForm.controls,
content: new FormControl(this.page?.content || ''), content: new FormControl(currentPage?.content || ''),
script: new FormControl(this.page?.script || ''), script: new FormControl(currentPage?.script || ''),
style: new FormControl(this.page?.style || ''), style: new FormControl(currentPage?.style || ''),
}); });
prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef); prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef);
} }
private executeSaveOperation(operation: 'save' | 'draft' | 'publish') { private executeSaveOperation(operation: 'save' | 'draft' | 'publish') {
if (this.isEditMode) { if (this.isEditMode()) {
if (!this.page || !this.pageId) { const currentPage = this.page();
if (!currentPage || !this.pageId) {
return; return;
} }
switch (operation) { switch (operation) {
case 'save': case 'save':
this.pageFormService.update(this.pageId, this.form, this.page).subscribe(); this.pageFormService.update(this.pageId, this.form, currentPage).subscribe();
break; break;
case 'draft': case 'draft':
this.pageFormService.updateAsDraft(this.pageId, this.form, this.page).subscribe(); this.pageFormService.updateAsDraft(this.pageId, this.form, currentPage).subscribe();
break; break;
case 'publish': case 'publish':
this.pageFormService.updateAndPublish(this.pageId, this.form, this.page).subscribe(); this.pageFormService.updateAndPublish(this.pageId, this.form, currentPage).subscribe();
break; break;
} }
return; return;

8
npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-list/page-list.component.html

@ -1,4 +1,4 @@
<abp-page [title]="'CmsKit::Pages' | abpLocalization" [toolbar]="data.items"> <abp-page [title]="'CmsKit::Pages' | abpLocalization" [toolbar]="data().items">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -21,6 +21,10 @@
</div> </div>
<div class="card"> <div class="card">
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> <abp-extensible-table
[data]="data().items"
[recordsTotal]="data().totalCount"
[list]="list"
/>
</div> </div>
</abp-page> </abp-page>

51
npm/ng-packs/packages/cms-kit/admin/src/components/pages/page-list/page-list.component.ts

@ -1,4 +1,5 @@
import { Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core';
@ -9,6 +10,7 @@ import { PageAdminService, GetPagesInputDto, PageDto } from '@abp/ng.cms-kit/pro
import { eCmsKitAdminComponents } from '../../../enums'; import { eCmsKitAdminComponents } from '../../../enums';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-page-list', selector: 'abp-page-list',
templateUrl: './page-list.component.html', templateUrl: './page-list.component.html',
providers: [ providers: [
@ -20,43 +22,36 @@ import { eCmsKitAdminComponents } from '../../../enums';
], ],
imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule], imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule],
}) })
export class PageListComponent implements OnInit { export class PageListComponent {
data: PagedResultDto<PageDto> = { items: [], totalCount: 0 };
public readonly list = inject(ListService<GetPagesInputDto>); public readonly list = inject(ListService<GetPagesInputDto>);
private pageService = inject(PageAdminService); private pageService = inject(PageAdminService);
private confirmationService = inject(ConfirmationService); private confirmationService = inject(ConfirmationService);
private toasterService = inject(ToasterService); private toasterService = inject(ToasterService);
filter = ''; readonly data = toSignal(
this.list.hookToQuery(query => {
let filters: Partial<GetPagesInputDto> = {};
if (this.list.filter) {
filters.filter = this.list.filter;
}
const input: GetPagesInputDto = {
...query,
...filters,
};
return this.pageService.getList(input);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<PageDto>,
},
);
ngOnInit() { filter = '';
this.hookToQuery();
}
onSearch() { onSearch() {
this.list.filter = this.filter; this.list.filter = this.filter;
this.list.get(); this.list.get();
} }
private hookToQuery() {
this.list
.hookToQuery(query => {
let filters: Partial<GetPagesInputDto> = {};
if (this.list.filter) {
filters.filter = this.list.filter;
}
const input: GetPagesInputDto = {
...query,
...filters,
};
return this.pageService.getList(input);
})
.subscribe(res => {
this.data = res;
});
}
delete(id: string) { delete(id: string) {
this.confirmationService this.confirmationService
.warn('CmsKit::PageDeletionConfirmationMessage', 'AbpUi::AreYouSure', { .warn('CmsKit::PageDeletionConfirmationMessage', 'AbpUi::AreYouSure', {
@ -65,9 +60,7 @@ export class PageListComponent implements OnInit {
}) })
.subscribe((status: Confirmation.Status) => { .subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) { if (status === Confirmation.Status.confirm) {
this.pageService.delete(id).subscribe(() => { this.pageService.delete(id).subscribe(() => this.list.get());
this.list.get();
});
} }
}); });
} }

8
npm/ng-packs/packages/cms-kit/admin/src/components/tags/tag-list/tag-list.component.html

@ -1,4 +1,4 @@
<abp-page [title]="'CmsKit::Tags' | abpLocalization" [toolbar]="data.items"> <abp-page [title]="'CmsKit::Tags' | abpLocalization" [toolbar]="data().items">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -21,10 +21,10 @@
</div> </div>
<div class="card"> <div class="card">
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> <abp-extensible-table [data]="data().items" [recordsTotal]="data().totalCount" [list]="list" />
</div> </div>
@if (isModalVisible) { @if (isModalVisible()) {
<abp-tag-modal [selected]="selected" (visibleChange)="onVisibleModalChange($event)" /> <abp-tag-modal [selected]="selected()" (visibleChange)="onVisibleModalChange($event)" />
} }
</abp-page> </abp-page>

77
npm/ng-packs/packages/cms-kit/admin/src/components/tags/tag-list/tag-list.component.ts

@ -1,4 +1,5 @@
import { Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core';
@ -10,6 +11,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { TagModalComponent, TagModalVisibleChange } from '../tag-modal/tag-modal.component'; import { TagModalComponent, TagModalVisibleChange } from '../tag-modal/tag-modal.component';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-tag-list', selector: 'abp-tag-list',
templateUrl: './tag-list.component.html', templateUrl: './tag-list.component.html',
providers: [ providers: [
@ -28,28 +30,35 @@ import { TagModalComponent, TagModalVisibleChange } from '../tag-modal/tag-modal
TagModalComponent, TagModalComponent,
], ],
}) })
export class TagListComponent implements OnInit { export class TagListComponent {
data: PagedResultDto<TagDto> = { items: [], totalCount: 0 };
public readonly list = inject(ListService<TagGetListInput>); public readonly list = inject(ListService<TagGetListInput>);
private tagService = inject(TagAdminService); private tagService = inject(TagAdminService);
private confirmationService = inject(ConfirmationService); private confirmationService = inject(ConfirmationService);
filter = ''; readonly data = toSignal(
isModalVisible = false; this.list.hookToQuery(query => {
selected?: TagDto; let filters: Partial<TagGetListInput> = {};
tagDefinitions: TagDefinitionDto[] = []; if (this.list.filter) {
filters.filter = this.list.filter;
}
const input: TagGetListInput = {
...query,
...filters,
};
return this.tagService.getList(input);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<TagDto>,
},
);
ngOnInit() { readonly tagDefinitions = toSignal(this.tagService.getTagDefinitions(), {
this.loadTagDefinitions(); initialValue: [] as TagDefinitionDto[],
this.hookToQuery(); });
}
private loadTagDefinitions() { filter = '';
this.tagService.getTagDefinitions().subscribe(definitions => { readonly isModalVisible = signal(false);
this.tagDefinitions = definitions; readonly selected = signal<TagDto | undefined>(undefined);
});
}
onSearch() { onSearch() {
this.list.filter = this.filter; this.list.filter = this.filter;
@ -57,35 +66,17 @@ export class TagListComponent implements OnInit {
} }
add() { add() {
this.selected = {} as TagDto; this.selected.set({} as TagDto);
this.isModalVisible = true; this.isModalVisible.set(true);
} }
edit(id: string) { edit(id: string) {
this.tagService.get(id).subscribe(tag => { this.tagService.get(id).subscribe(tag => {
this.selected = tag; this.selected.set(tag);
this.isModalVisible = true; this.isModalVisible.set(true);
}); });
} }
private hookToQuery() {
this.list
.hookToQuery(query => {
let filters: Partial<TagGetListInput> = {};
if (this.list.filter) {
filters.filter = this.list.filter;
}
const input: TagGetListInput = {
...query,
...filters,
};
return this.tagService.getList(input);
})
.subscribe(res => {
this.data = res;
});
}
onVisibleModalChange(visibilityChange: TagModalVisibleChange) { onVisibleModalChange(visibilityChange: TagModalVisibleChange) {
if (visibilityChange.visible) { if (visibilityChange.visible) {
return; return;
@ -93,8 +84,8 @@ export class TagListComponent implements OnInit {
if (visibilityChange.refresh) { if (visibilityChange.refresh) {
this.list.get(); this.list.get();
} }
this.selected = null; this.selected.set(undefined);
this.isModalVisible = false; this.isModalVisible.set(false);
} }
delete(id: string, name: string) { delete(id: string, name: string) {
@ -104,9 +95,7 @@ export class TagListComponent implements OnInit {
}) })
.subscribe((status: Confirmation.Status) => { .subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) { if (status === Confirmation.Status.confirm) {
this.tagService.delete(id).subscribe(() => { this.tagService.delete(id).subscribe(() => this.list.get());
this.list.get();
});
} }
}); });
} }

11
npm/ng-packs/packages/cms-kit/admin/src/components/tags/tag-modal/tag-modal.component.ts

@ -1,4 +1,12 @@
import { Component, OnInit, inject, Injector, input, output } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
inject,
Injector,
input,
OnInit,
output,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormGroup } from '@angular/forms'; import { ReactiveFormsModule, FormGroup } from '@angular/forms';
import { NgxValidateCoreModule } from '@ngx-validate/core'; import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -22,6 +30,7 @@ export interface TagModalVisibleChange {
} }
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-tag-modal', selector: 'abp-tag-modal',
templateUrl: './tag-modal.component.html', templateUrl: './tag-modal.component.html',
imports: [ imports: [

10
npm/ng-packs/packages/cms-kit/src/components/code-mirror-editor/code-mirror-editor.component.ts

@ -1,10 +1,18 @@
import { Component, ElementRef, AfterViewInit, ViewChild, forwardRef } from '@angular/core'; import {
Component,
ElementRef,
AfterViewInit,
ViewChild,
forwardRef,
ChangeDetectionStrategy,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { EditorView, lineNumbers } from '@codemirror/view'; import { EditorView, lineNumbers } from '@codemirror/view';
import { basicSetup } from 'codemirror'; import { basicSetup } from 'codemirror';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-codemirror-editor', selector: 'abp-codemirror-editor',
template: `<div #cmHost class="codemirror-container"></div>`, template: `<div #cmHost class="codemirror-container"></div>`,
providers: [ providers: [

8
npm/ng-packs/packages/cms-kit/src/components/toast-ui/toastui-editor.component.ts

@ -2,14 +2,15 @@ import { isPlatformBrowser } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
Component,
AfterViewInit, AfterViewInit,
ViewChild, ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef, ElementRef,
forwardRef, forwardRef,
inject, inject,
PLATFORM_ID, PLATFORM_ID,
DestroyRef, ViewChild,
} from '@angular/core'; } from '@angular/core';
import Editor from '@toast-ui/editor'; import Editor from '@toast-ui/editor';
@ -17,6 +18,7 @@ import { AbpLocalStorageService } from '@abp/ng.core';
import { THEME_CHANGE_TOKEN } from '@abp/ng.theme.shared'; import { THEME_CHANGE_TOKEN } from '@abp/ng.theme.shared';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-toastui-editor', selector: 'abp-toastui-editor',
template: `<div #editorContainer></div>`, template: `<div #editorContainer></div>`,
providers: [ providers: [

Loading…
Cancel
Save