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">
<div class="card">
<div class="card-body">
@if (form && (!isEditMode || blogPost)) {
@if (form && (!isEditMode() || blogPost())) {
<form [formGroup]="form" (ngSubmit)="saveAsDraft()" [validateOnSubmit]="true">
<!-- Cover Image -->
<div class="mb-3">
@if (coverImagePreview) {
@if (coverImagePreview()) {
<div class="mb-2">
<img [src]="coverImagePreview" height="120" alt="Cover Image" />
<img [src]="coverImagePreview()" height="120" alt="Cover Image" />
<br />
<button type="button" class="btn btn-link p-0" (click)="removeCoverImage()">
{{ 'CmsKit::RemoveCoverImage' | abpLocalization }}
@ -28,7 +28,7 @@
</div>
<!-- Basic fields -->
<abp-extensible-form [selectedRecord]="blogPost || {}" />
<abp-extensible-form [selectedRecord]="blogPost() || {}" />
<!-- Content editor -->
<div class="mt-3">
@ -38,14 +38,15 @@
</form>
<!-- Tags Editor -->
@if (isTagsEnabled) {
@if (isTagsEnabled()) {
<hr />
<div class="mb-3">
<label class="form-label">{{ 'CmsKit::Tags' | abpLocalization }}</label>
<input
type="text"
class="form-control"
[(ngModel)]="tags"
[ngModel]="tags()"
(ngModelChange)="tags.set($event)"
[ngModelOptions]="{ standalone: true }"
[placeholder]="'CmsKit::TagsHelpText' | abpLocalization"
/>
@ -63,7 +64,7 @@
<button class="btn btn-secondary" (click)="saveAsDraft()" [disabled]="form?.invalid">
{{ 'CmsKit::SaveAsDraft' | abpLocalization }}
</button>
@if (isEditMode) {
@if (isEditMode()) {
<abp-button (click)="publish()" [disabled]="form?.invalid">
{{ 'CmsKit::Publish' | abpLocalization }}
</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 { 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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -29,6 +37,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { BlogPostFormService } from '../../../services';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-post-form',
templateUrl: './blog-post-form.component.html',
providers: [
@ -60,33 +69,33 @@ export class BlogPostFormComponent implements OnInit {
private destroyRef = inject(DestroyRef);
form: FormGroup;
blogPost: BlogPostDto | null = null;
readonly blogPost = signal<BlogPostDto | null>(null);
blogPostId: string | null = null;
isEditMode = false;
readonly isEditMode = signal(false);
coverImageFile: File | null = null;
coverImagePreview: string | null = null;
tags: string = '';
isTagsEnabled = true;
readonly coverImagePreview = signal<string | null>(null);
readonly tags = signal('');
readonly isTagsEnabled = signal(true);
readonly BLOG_POST_ENTITY_TYPE = 'BlogPost';
ngOnInit() {
const id = this.route.snapshot.params['id'];
if (id) {
this.isEditMode = true;
this.isEditMode.set(true);
this.blogPostId = id;
this.loadBlogPost(id);
} else {
this.isEditMode = false;
this.isEditMode.set(false);
this.buildForm();
}
}
private loadBlogPost(id: string) {
this.blogPostService.get(id).subscribe(blogPost => {
this.blogPost = blogPost;
this.blogPost.set(blogPost);
if (blogPost.coverImageMediaId) {
this.coverImagePreview = `/api/cms-kit/media/${blogPost.coverImageMediaId}`;
this.coverImagePreview.set(`/api/cms-kit/media/${blogPost.coverImageMediaId}`);
}
this.buildForm();
this.loadTags(id);
@ -94,7 +103,6 @@ export class BlogPostFormComponent implements OnInit {
}
private loadTags(blogPostId: string) {
// TODO: use the public service to load the tags
this.restService
.request<void, TagDto[]>({
method: 'GET',
@ -102,28 +110,27 @@ export class BlogPostFormComponent implements OnInit {
})
.subscribe(tags => {
if (tags && tags.length > 0) {
this.tags = tags.map(t => t.name || '').join(', ');
this.tags.set(tags.map(t => t.name || '').join(', '));
}
});
}
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);
this.form = new FormGroup({
...baseForm.controls,
content: new FormControl(this.blogPost?.content || ''),
coverImageMediaId: new FormControl(this.blogPost?.coverImageMediaId || null),
content: new FormControl(currentBlogPost?.content || ''),
coverImageMediaId: new FormControl(currentBlogPost?.coverImageMediaId || null),
});
prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef);
// Check if tags feature is enabled for the blog
const blogId = this.form.get('blogId')?.value || this.blogPost?.blogId;
const blogId = this.form.get('blogId')?.value || currentBlogPost?.blogId;
if (blogId) {
this.checkTagsFeature(blogId);
}
// Listen for blog selection changes
this.form
.get('blogId')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
@ -142,7 +149,7 @@ export class BlogPostFormComponent implements OnInit {
})
.subscribe(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];
const reader = new FileReader();
reader.onload = (e: any) => {
this.coverImagePreview = e.target.result;
this.coverImagePreview.set(e.target.result);
};
reader.readAsDataURL(this.coverImageFile);
}
@ -160,7 +167,7 @@ export class BlogPostFormComponent implements OnInit {
removeCoverImage() {
this.coverImageFile = null;
this.coverImagePreview = null;
this.coverImagePreview.set(null);
this.form.patchValue({ coverImageMediaId: null });
}
@ -183,11 +190,12 @@ export class BlogPostFormComponent implements OnInit {
}
private setTags(blogPostId: string) {
if (!this.tags || !this.tags.trim()) {
const tagsValue = this.tags();
if (!tagsValue || !tagsValue.trim()) {
return of(null);
}
const tagArray = this.tags
const tagArray = tagsValue
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
@ -204,7 +212,6 @@ export class BlogPostFormComponent implements OnInit {
}
private executeSaveOperation(operation: 'save' | 'draft' | 'publish' | 'sendToReview') {
// First upload cover image if selected
this.uploadCoverImage()
.pipe(
tap(coverImageMediaId => {
@ -213,11 +220,12 @@ export class BlogPostFormComponent implements OnInit {
}
}),
switchMap(() => {
if (this.isEditMode) {
if (!this.blogPost || !this.blogPostId) {
if (this.isEditMode()) {
const currentBlogPost = this.blogPost();
if (!currentBlogPost || !this.blogPostId) {
return of(null);
}
return this.blogPostFormService.update(this.blogPostId, this.form, this.blogPost);
return this.blogPostFormService.update(this.blogPostId, this.form, currentBlogPost);
}
switch (operation) {
@ -236,7 +244,6 @@ export class BlogPostFormComponent implements OnInit {
if (!result || !result.id) {
return of(null);
}
// Set tags after blog post is created/updated
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-body">
<div class="row">
@ -35,6 +35,10 @@
</div>
<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>
</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 { FormsModule } from '@angular/forms';
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core';
@ -14,6 +15,7 @@ import {
import { eCmsKitAdminComponents } from '../../../enums';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-post-list',
templateUrl: './blog-post-list.component.html',
providers: [
@ -25,21 +27,35 @@ import { eCmsKitAdminComponents } from '../../../enums';
],
imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule],
})
export class BlogPostListComponent implements OnInit {
data: PagedResultDto<BlogPostListDto> = { items: [], totalCount: 0 };
export class BlogPostListComponent {
public readonly list = inject(ListService<BlogPostGetListInput>);
private blogPostService = inject(BlogPostAdminService);
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 = '';
statusFilter: BlogPostStatus | null = null;
BlogPostStatus = BlogPostStatus;
ngOnInit() {
this.hookToQuery();
}
onSearch() {
this.list.filter = this.filter;
this.list.get();
@ -49,25 +65,6 @@ export class BlogPostListComponent implements OnInit {
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) {
this.confirmationService
.warn('CmsKit::BlogPostDeletionConfirmationMessage', 'AbpUi::AreYouSure', {
@ -77,9 +74,7 @@ export class BlogPostListComponent implements OnInit {
})
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.blogPostService.delete(id).subscribe(() => {
this.list.get();
});
this.blogPostService.delete(id).subscribe(() => 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 #abpBody>
@if (form) {
<form [formGroup]="form" (ngSubmit)="save()" [validateOnSubmit]="true">
@if (form(); as currentForm) {
<form [formGroup]="currentForm" (ngSubmit)="save()" [validateOnSubmit]="true">
<div formArrayName="features">
@for (featureControl of featuresFormArray.controls; track $index; let i = $index) {
<div class="mb-3" [formGroupName]="i">
@ -19,7 +19,7 @@
formControlName="isEnabled"
/>
<label class="form-check-label" for="feature-{{ i }}">
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }}
{{ 'CmsKit::' + features()[i]?.featureName | abpLocalization }}
</label>
</div>
} @else {
@ -36,7 +36,7 @@
[disabled]="true"
/>
<label class="form-check-label" for="feature-{{ i }}">
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }}
{{ 'CmsKit::' + features()[i]?.featureName | abpLocalization }}
</label>
</div>
</div>
@ -58,7 +58,7 @@
<button type="button" class="btn btn-outline-primary" abpClose>
{{ 'AbpUi::Cancel' | abpLocalization }}
</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 }}
</abp-button>
</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 { ReactiveFormsModule, FormArray, FormBuilder, FormGroup } from '@angular/forms';
import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -22,6 +30,7 @@ export interface BlogFeaturesModalVisibleChange {
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-features-modal',
templateUrl: './blog-features-modal.component.html',
imports: [
@ -42,8 +51,8 @@ export class BlogFeaturesModalComponent implements OnInit {
blogId = input<string>();
visibleChange = output<BlogFeaturesModalVisibleChange>();
form: FormGroup;
features: BlogFeatureDto[] = [];
readonly form = signal<FormGroup | undefined>(undefined);
readonly features = signal<BlogFeatureDto[]>([]);
private initialFeatureStates: Map<string, boolean> = new Map();
ngOnInit() {
@ -54,19 +63,19 @@ export class BlogFeaturesModalComponent implements OnInit {
private loadFeatures() {
this.blogFeatureService.getList(this.blogId()!).subscribe(features => {
this.features = features.sort((a, b) =>
const sorted = features.sort((a, b) =>
(a.featureName || '').localeCompare(b.featureName || ''),
);
// Store initial states
this.features.set(sorted);
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() {
const featureControls = this.features.map(feature =>
private buildForm(features: BlogFeatureDto[]) {
const featureControls = features.map(feature =>
this.fb.group({
featureName: [feature.featureName],
isEnabled: [feature.isEnabled],
@ -74,13 +83,15 @@ export class BlogFeaturesModalComponent implements OnInit {
}),
);
this.form = this.fb.group({
features: this.fb.array(featureControls),
});
this.form.set(
this.fb.group({
features: this.fb.array(featureControls),
}),
);
}
get featuresFormArray(): FormArray {
return this.form.get('features') as FormArray;
return this.form()!.get('features') as FormArray;
}
onVisibleChange(visible: boolean, refresh = false) {
@ -88,20 +99,19 @@ export class BlogFeaturesModalComponent implements OnInit {
}
save() {
if (!this.form.valid || !this.blogId()) {
const currentForm = this.form();
if (!currentForm?.valid || !this.blogId()) {
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
.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,
@ -112,23 +122,18 @@ export class BlogFeaturesModalComponent implements OnInit {
})
.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');
},
forkJoin(saveObservables).subscribe(() => {
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">
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" />
<abp-extensible-table [data]="data().items" [recordsTotal]="data().totalCount" [list]="list" />
</div>
@if (isModalVisible) {
<abp-blog-modal [selected]="selected" (visibleChange)="onVisibleModalChange($event)" />
@if (isModalVisible()) {
<abp-blog-modal [selected]="selected()" (visibleChange)="onVisibleModalChange($event)" />
}
@if (isFeaturesModalVisible) {
@if (isFeaturesModalVisible()) {
<abp-blog-features-modal
[blogId]="selectedBlogId"
[blogId]="selectedBlogId()"
(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 { FormsModule } from '@angular/forms';
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core';
@ -14,6 +15,7 @@ import {
} from '../blog-features-modal/blog-features-modal.component';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-list',
templateUrl: './blog-list.component.html',
providers: [
@ -33,22 +35,33 @@ import {
BlogFeaturesModalComponent,
],
})
export class BlogListComponent implements OnInit {
data: PagedResultDto<BlogDto> = { items: [], totalCount: 0 };
export class BlogListComponent {
public readonly list = inject(ListService<BlogGetListInput>);
private blogService = inject(BlogAdminService);
private confirmationService = inject(ConfirmationService);
filter = '';
isModalVisible = false;
selected?: BlogDto;
isFeaturesModalVisible = false;
selectedBlogId?: string;
readonly data = toSignal(
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);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<BlogDto>,
},
);
ngOnInit() {
this.hookToQuery();
}
filter = '';
readonly isModalVisible = signal(false);
readonly selected = signal<BlogDto | undefined>(undefined);
readonly isFeaturesModalVisible = signal(false);
readonly selectedBlogId = signal<string | undefined>(undefined);
onSearch() {
this.list.filter = this.filter;
@ -56,14 +69,14 @@ export class BlogListComponent implements OnInit {
}
add() {
this.selected = {} as BlogDto;
this.isModalVisible = true;
this.selected.set({} as BlogDto);
this.isModalVisible.set(true);
}
edit(id: string) {
this.blogService.get(id).subscribe(blog => {
this.selected = blog;
this.isModalVisible = true;
this.selected.set(blog);
this.isModalVisible.set(true);
});
}
@ -74,34 +87,14 @@ export class BlogListComponent implements OnInit {
})
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.blogService.delete(id).subscribe(() => {
this.list.get();
});
this.blogService.delete(id).subscribe(() => this.list.get());
}
});
}
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;
});
this.selectedBlogId.set(id);
this.isFeaturesModalVisible.set(true);
}
onVisibleModalChange(visibilityChange: BlogModalVisibleChange) {
@ -111,8 +104,8 @@ export class BlogListComponent implements OnInit {
if (visibilityChange.refresh) {
this.list.get();
}
this.selected = null;
this.isModalVisible = false;
this.selected.set(undefined);
this.isModalVisible.set(false);
}
onFeaturesModalChange(visibilityChange: BlogFeaturesModalVisibleChange) {
@ -122,7 +115,7 @@ export class BlogListComponent implements OnInit {
if (visibilityChange.refresh) {
this.list.get();
}
this.selectedBlogId = null;
this.isFeaturesModalVisible = false;
this.selectedBlogId.set(undefined);
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>
<h3>
{{ (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 { ReactiveFormsModule, FormGroup } from '@angular/forms';
import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -24,6 +34,7 @@ export interface BlogModalVisibleChange {
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-blog-modal',
templateUrl: './blog-modal.component.html',
imports: [
@ -46,7 +57,7 @@ export class BlogModalComponent implements OnInit {
selected = input<BlogDto>();
visibleChange = output<BlogModalVisibleChange>();
modalBusy = false;
readonly modalBusy = signal(false);
form: FormGroup;
@ -65,12 +76,13 @@ export class BlogModalComponent implements OnInit {
}
save() {
if (this.modalBusy) {
if (this.modalBusy()) {
return;
}
this.modalBusy = true;
this.modalBusy.set(true);
if (!this.form.valid) {
this.modalBusy.set(false);
return;
}
@ -86,7 +98,7 @@ export class BlogModalComponent implements OnInit {
} as UpdateBlogDto);
}
observable$.pipe(finalize(() => (this.modalBusy = false))).subscribe(() => {
observable$.pipe(finalize(() => this.modalBusy.set(false))).subscribe(() => {
this.onVisibleChange(false, true);
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>
<hr class="my-3" />
<div>
<abp-button (click)="submit()" iconClass="fa fa-save" [loading]="loading">
<abp-button (click)="submit()" iconClass="fa fa-save" [loading]="loading()">
{{ 'AbpAccount::Save' | abpLocalization }}
</abp-button>
</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 {
NgbNav,
@ -33,12 +33,12 @@ import { CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT } from '../comments';
ReactiveFormsModule,
],
})
export class CmsSettingsComponent {
export class CmsSettingsComponent implements OnInit {
readonly commentAdminService = inject(CommentAdminService);
readonly configState = inject(ConfigStateService);
readonly toaster = inject(ToasterService);
loading = false;
readonly loading = signal(false);
commentApprovalControl = new FormControl(false);
@ -49,12 +49,12 @@ export class CmsSettingsComponent {
}
submit() {
this.loading = true;
this.loading.set(true);
this.commentAdminService
.updateSettings({ commentRequireApprovement: this.commentApprovalControl.value })
.pipe(
finalize(() => {
this.loading = false;
this.loading.set(false);
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">
@if (comment) {
@if (comment(); as comment) {
<div class="card mb-4">
<div class="card-body">
<table class="table">
@ -59,7 +59,7 @@
<div class="card mb-4">
<div class="card-body">
<form [formGroup]="filterForm" (ngSubmit)="onFilter()">
<input type="hidden" [value]="commentId" />
<input type="hidden" [value]="commentId()" />
<div class="row">
<div class="col-lg-4 col-md-12">
<div class="row">
@ -142,6 +142,6 @@
<h3>{{ 'CmsKit::RepliesToThisComment' | abpLocalization }}</h3>
<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>
</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 { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
@ -18,6 +19,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { CommentEntityService } from '../../../services';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-comment-details',
templateUrl: './comment-details.component.html',
providers: [
@ -46,8 +48,8 @@ import { CommentEntityService } from '../../../services';
],
})
export class CommentDetailsComponent implements OnInit {
comment: CommentWithAuthorDto | null = null;
data: PagedResultDto<CommentWithAuthorDto> = { items: [], totalCount: 0 };
readonly comment = signal<CommentWithAuthorDto | null>(null);
readonly commentId = signal<string | null>(null);
readonly list = inject(ListService<CommentGetListInput>);
readonly commentEntityService = inject(CommentEntityService);
@ -57,19 +59,33 @@ export class CommentDetailsComponent implements OnInit {
private router = inject(Router);
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;
commentApproveStateOptions = commentApproveStateOptions;
commentId!: string;
requireApprovement: boolean;
ngOnInit() {
this.route.params.subscribe(params => {
const id = params['id'];
if (id) {
this.commentId = id;
this.commentId.set(id);
this.loadComment(id);
this.createFilterForm();
this.hookToQuery();
this.list.get();
}
});
this.requireApprovement = this.commentEntityService.requireApprovement;
@ -86,7 +102,7 @@ export class CommentDetailsComponent implements OnInit {
private loadComment(id: string) {
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> = {
author: formValue.author || undefined,
commentApproveState: formValue.commentApproveState,
repliedCommentId: this.commentId,
repliedCommentId: this.commentId() ?? undefined,
creationStartDate: formValue.creationStartDate || undefined,
creationEndDate: formValue.creationEndDate || undefined,
};
@ -104,20 +120,6 @@ export class CommentDetailsComponent implements OnInit {
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) {
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 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>
</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 { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { NgbDateAdapter, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
@ -17,6 +18,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { CommentEntityService } from '../../../services';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-comment-list',
templateUrl: './comment-list.component.html',
providers: [
@ -44,21 +46,32 @@ import { CommentEntityService } from '../../../services';
],
})
export class CommentListComponent implements OnInit {
data: PagedResultDto<CommentWithAuthorDto> = { items: [], totalCount: 0 };
readonly list = inject(ListService<CommentGetListInput>);
readonly commentEntityService = inject(CommentEntityService);
private commentService = inject(CommentAdminService);
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;
commentApproveStateOptions = commentApproveStateOptions;
requireApprovement: boolean;
ngOnInit() {
this.createFilterForm();
this.hookToQuery();
this.requireApprovement = this.commentEntityService.requireApprovement;
}
@ -85,17 +98,4 @@ export class CommentListComponent implements OnInit {
this.list.filter = filters as any;
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
#nav="ngbNav"
class="nav-tabs"
[(activeId)]="activeTab"
[activeId]="activeTab()"
(activeIdChange)="onTabChange($event)"
>
<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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
@ -15,6 +22,7 @@ import {
} from '@abp/ng.cms-kit/proxy';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-global-resources',
imports: [
CommonModule,
@ -34,7 +42,7 @@ export class GlobalResourcesComponent implements OnInit {
private destroyRef = inject(DestroyRef);
form: FormGroup;
activeTab: string = 'script';
readonly activeTab = signal('script');
ngOnInit() {
this.buildForm();
@ -66,7 +74,7 @@ export class GlobalResourcesComponent implements OnInit {
}
onTabChange(activeId: string) {
this.activeTab = activeId;
this.activeTab.set(activeId);
}
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-body">
@if (nodes.length > 0) {
@if (nodes().length > 0) {
<abp-tree
[nodes]="nodes"
[draggable]="draggable"
[expandedKeys]="expandedKeys"
[selectedNode]="selectedNode"
[nodes]="nodes()"
[draggable]="draggable()"
[expandedKeys]="expandedKeys()"
[selectedNode]="selectedNode()"
(selectedNodeChange)="onSelectedNodeChange($event)"
[beforeDrop]="beforeDrop"
(dropOver)="onDrop($event)"
@ -47,10 +47,10 @@
</div>
</abp-page>
@if (isModalVisible) {
@if (isModalVisible()) {
<abp-menu-item-modal
[selected]="selectedMenuItem || undefined"
[parentId]="parentId"
[selected]="selectedMenuItem() || undefined"
[parentId]="parentId()"
(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 { of } from 'rxjs';
import { PageComponent } from '@abp/ng.components/page';
@ -19,6 +19,7 @@ import {
} from '../menu-item-modal/menu-item-modal.component';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-menu-item-list',
templateUrl: './menu-item-list.component.html',
imports: [
@ -41,13 +42,13 @@ export class MenuItemListComponent implements OnInit {
private menuItemService = inject(MenuItemAdminService);
private confirmationService = inject(ConfirmationService);
nodes: any[] = [];
selectedNode: MenuItemDto | null = null;
expandedKeys: string[] = [];
draggable = true;
isModalVisible = false;
selectedMenuItem: MenuItemDto | MenuItemWithDetailsDto | null = null;
parentId: string | null = null;
readonly nodes = signal<any[]>([]);
readonly selectedNode = signal<MenuItemDto | null>(null);
readonly expandedKeys = signal<string[]>([]);
readonly draggable = signal(true);
readonly isModalVisible = signal(false);
readonly selectedMenuItem = signal<MenuItemDto | MenuItemWithDetailsDto | null>(null);
readonly parentId = signal<string | null>(null);
ngOnInit() {
this.loadMenuItems();
@ -56,11 +57,12 @@ export class MenuItemListComponent implements OnInit {
private loadMenuItems() {
this.menuItemService.getList().subscribe(result => {
if (result.items && result.items.length > 0) {
this.nodes = this.buildTreeNodes(result.items);
// Expand all nodes by default
this.expandedKeys = this.nodes.map(n => n.key);
const treeNodes = this.buildTreeNodes(result.items);
this.nodes.set(treeNodes);
this.expandedKeys.set(treeNodes.map(n => n.key));
} 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 rootNodes: any[] = [];
// First pass: create all nodes
items.forEach(item => {
const node: any = {
key: item.id,
@ -81,7 +82,6 @@ export class MenuItemListComponent implements OnInit {
nodeMap.set(item.id!, node);
});
// Second pass: build tree structure
items.forEach(item => {
const node = nodeMap.get(item.id!);
if (item.parentId) {
@ -97,7 +97,6 @@ export class MenuItemListComponent implements OnInit {
}
});
// Sort by order
const sortByOrder = (nodes: any[]) => {
nodes.sort((a, b) => (a.entity.order || 0) - (b.entity.order || 0));
nodes.forEach(node => {
@ -112,7 +111,7 @@ export class MenuItemListComponent implements OnInit {
}
onSelectedNodeChange(node: any) {
this.selectedNode = node?.entity || null;
this.selectedNode.set(node?.entity || null);
}
onDrop(event: any) {
@ -143,16 +142,10 @@ export class MenuItemListComponent implements OnInit {
};
this.menuItemService.moveMenuItem(node.id!, input).subscribe({
next: () => {
this.loadMenuItems();
},
error: () => {
// Reload to rollback
this.loadMenuItems();
},
next: () => this.loadMenuItems(),
error: () => this.loadMenuItems(),
});
} else {
// Reload to rollback
this.loadMenuItems();
}
});
@ -163,22 +156,22 @@ export class MenuItemListComponent implements OnInit {
};
add() {
this.selectedMenuItem = null;
this.parentId = null;
this.isModalVisible = true;
this.selectedMenuItem.set(null);
this.parentId.set(null);
this.isModalVisible.set(true);
}
addSubMenuItem(parentId?: string) {
this.selectedMenuItem = null;
this.parentId = parentId || null;
this.isModalVisible = true;
this.selectedMenuItem.set(null);
this.parentId.set(parentId || null);
this.isModalVisible.set(true);
}
edit(id: string) {
this.menuItemService.get(id).subscribe(menuItem => {
this.selectedMenuItem = menuItem;
this.parentId = null;
this.isModalVisible = true;
this.selectedMenuItem.set(menuItem);
this.parentId.set(null);
this.isModalVisible.set(true);
});
}
@ -189,9 +182,9 @@ export class MenuItemListComponent implements OnInit {
if (visibilityChange.refresh) {
this.loadMenuItems();
}
this.selectedMenuItem = null;
this.parentId = null;
this.isModalVisible = false;
this.selectedMenuItem.set(null);
this.parentId.set(null);
this.isModalVisible.set(false);
}
delete(id: string, displayName?: string) {
@ -204,9 +197,7 @@ export class MenuItemListComponent implements OnInit {
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.menuItemService.delete(id).subscribe({
next: () => {
this.loadMenuItems();
},
next: () => 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
#nav="ngbNav"
class="nav-tabs"
[(activeId)]="activeTab"
[activeId]="activeTab()"
(activeIdChange)="onTabChange($event)"
>
<li ngbNavItem="url">
@ -51,9 +51,9 @@
type="button"
id="pageSelectDropdown"
ngbDropdownToggle
[class.text-muted]="!selectedPage"
[class.text-muted]="!selectedPage()"
>
<span>{{ selectedPage?.title || ('CmsKit::Page' | abpLocalization) }}</span>
<span>{{ selectedPage()?.title || ('CmsKit::Page' | abpLocalization) }}</span>
</button>
<div
class="dropdown-menu w-100"
@ -65,16 +65,16 @@
<input
type="text"
class="form-control form-control-sm"
[(ngModel)]="pageSearchText"
[ngModel]="pageSearchText()"
(ngModelChange)="onPageSearchChange($event)"
[ngModelOptions]="{ standalone: true }"
(input)="onPageSearchChange($any($event.target).value)"
(click)="$event.stopPropagation()"
placeholder="{{ 'AbpUi::Search' | abpLocalization }}"
autocomplete="off"
/>
</div>
@if (filteredPages.length > 0) {
@for (page of filteredPages; track page.id) {
@if (filteredPages().length > 0) {
@for (page of filteredPages(); track page.id) {
<button
type="button"
class="dropdown-item"
@ -83,16 +83,16 @@
{{ page.title }}
</button>
}
} @else if (pageSearchText && pageSearchText.length > 0) {
} @else if (pageSearchText() && pageSearchText().length > 0) {
<div class="dropdown-item text-muted">
{{ 'No results found' }}
</div>
} @else if (pages.length === 0) {
} @else if (pages().length === 0) {
<div class="dropdown-item text-muted">
{{ 'No pages found' }}
</div>
}
@if (selectedPage) {
@if (selectedPage()) {
<div class="dropdown-divider"></div>
<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 { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormGroup, FormControl, FormsModule } from '@angular/forms';
@ -43,6 +53,7 @@ const TABS = {
} as const;
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-menu-item-modal',
templateUrl: './menu-item-modal.component.html',
imports: [
@ -81,13 +92,13 @@ export class MenuItemModalComponent implements OnInit {
// Form state
form: FormGroup;
activeTab: string = TABS.URL;
readonly activeTab = signal<string>(TABS.URL);
// Page selection state
pages: PageLookupDto[] = [];
selectedPage: PageLookupDto | null = null;
pageSearchText: string = '';
filteredPages: PageLookupDto[] = [];
// Page search state
readonly pages = signal<PageLookupDto[]>([]);
readonly selectedPage = signal<PageLookupDto | null>(null);
readonly pageSearchText = signal('');
readonly filteredPages = signal<PageLookupDto[]>([]);
// Search subject for debouncing
private readonly pageSearchSubject = new Subject<string>();
@ -123,10 +134,10 @@ export class MenuItemModalComponent implements OnInit {
)
.subscribe({
next: result => {
this.filteredPages = result.items || [];
this.filteredPages.set(result.items || []);
},
error: () => {
this.filteredPages = [];
this.filteredPages.set([]);
},
});
}
@ -156,8 +167,8 @@ export class MenuItemModalComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: ({ menuItem, pages }) => {
this.pages = pages.items || [];
this.filteredPages = this.pages;
this.pages.set(pages.items || []);
this.filteredPages.set(pages.items || []);
this.buildForm(menuItem);
this.initializePageSelection(menuItem);
},
@ -176,8 +187,8 @@ export class MenuItemModalComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: result => {
this.pages = result.items || [];
this.filteredPages = this.pages;
this.pages.set(result.items || []);
this.filteredPages.set(result.items || []);
},
error: () => {
this.toasterService.error('AbpUi::ErrorMessage');
@ -190,16 +201,15 @@ export class MenuItemModalComponent implements OnInit {
*/
private initializePageSelection(menuItem: MenuItemWithDetailsDto | MenuItemDto): void {
if (menuItem.pageId) {
this.activeTab = TABS.PAGE;
this.selectedPage = this.pages.find(p => p.id === menuItem.pageId) || null;
this.activeTab.set(TABS.PAGE);
const page = this.pages().find(p => p.id === menuItem.pageId) || null;
this.selectedPage.set(page);
const url = this.selectedPage
? this.generateUrlFromPage(this.selectedPage)
: menuItem.url || '';
const url = page ? this.generateUrlFromPage(page) : menuItem.url || '';
this.form.patchValue({ pageId: menuItem.pageId, url }, { emitEvent: false });
this.pageSearchText = this.selectedPage?.title || '';
this.pageSearchText.set(page?.title || '');
} else if (menuItem.url) {
this.activeTab = TABS.URL;
this.activeTab.set(TABS.URL);
this.form.patchValue({ url: menuItem.url, pageId: null }, { emitEvent: false });
}
}
@ -220,9 +230,9 @@ export class MenuItemModalComponent implements OnInit {
* Handles page search input changes
*/
onPageSearchChange(searchText: string): void {
this.pageSearchText = searchText;
this.pageSearchText.set(searchText);
if (!searchText?.trim()) {
this.filteredPages = this.pages;
this.filteredPages.set(this.pages());
return;
}
this.pageSearchSubject.next(searchText);
@ -232,8 +242,8 @@ export class MenuItemModalComponent implements OnInit {
* Handles dropdown open event
*/
onDropdownOpen(): void {
if (!this.pageSearchText?.trim()) {
this.filteredPages = this.pages;
if (!this.pageSearchText()?.trim()) {
this.filteredPages.set(this.pages());
}
}
@ -243,11 +253,11 @@ export class MenuItemModalComponent implements OnInit {
selectPage(page: PageLookupDto): void {
if (!page) return;
this.selectedPage = page;
this.selectedPage.set(page);
const url = this.generateUrlFromPage(page);
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 {
this.form.patchValue({ pageId: null }, { emitEvent: false });
this.selectedPage = null;
this.pageSearchText = '';
this.filteredPages = this.pages;
this.selectedPage.set(null);
this.pageSearchText.set('');
this.filteredPages.set(this.pages());
}
/**
@ -282,7 +292,7 @@ export class MenuItemModalComponent implements OnInit {
* Loads the available menu order for new menu items
*/
private loadAvailableOrder(parentId: string | null, menuItemId?: string): void {
if (menuItemId) return; // Only needed for new items
if (menuItemId) return;
const order$ = parentId
? this.menuItemService.getAvailableMenuOrder(parentId)
@ -306,7 +316,7 @@ export class MenuItemModalComponent implements OnInit {
* Handles tab changes
*/
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;
if (urlValue && this.form.get('pageId')?.value) {
this.clearPageSelection();
if (this.activeTab === TABS.PAGE) {
this.activeTab = TABS.URL;
if (this.activeTab() === TABS.PAGE) {
this.activeTab.set(TABS.URL);
}
}
}
@ -356,17 +366,14 @@ export class MenuItemModalComponent implements OnInit {
const formValue = { ...this.form.value };
if (formValue.pageId) {
// If pageId is set, generate URL from the page
const selectedPage = this.pages.find(p => p.id === formValue.pageId);
if (selectedPage) {
formValue.url = this.generateUrlFromPage(selectedPage);
const page = this.pages().find(p => p.id === formValue.pageId);
if (page) {
formValue.url = this.generateUrlFromPage(page);
}
} else if (formValue.url) {
// If URL is manually entered, ensure pageId is cleared
formValue.pageId = null;
}
// Clean up undefined values
return {
...formValue,
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">
<div class="card">
<div class="card-body">
@if (form && (!isEditMode || page)) {
@if (form && (!isEditMode() || page())) {
<form [formGroup]="form" (ngSubmit)="save()" [validateOnSubmit]="true">
<!-- Basic fields outside tabs -->
<abp-extensible-form [selectedRecord]="page || {}" />
<abp-extensible-form [selectedRecord]="page() || {}" />
<!-- Tabs for Content, Script, and Style -->
<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 { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
@ -23,6 +31,7 @@ import { eCmsKitAdminComponents } from '../../../enums';
import { PageFormService } from '../../../services';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-page-form',
templateUrl: './page-form.component.html',
providers: [
@ -52,56 +61,58 @@ export class PageFormComponent implements OnInit {
private destroyRef = inject(DestroyRef);
form: FormGroup;
page: PageDto | null = null;
readonly page = signal<PageDto | null>(null);
pageId: string | null = null;
isEditMode = false;
readonly isEditMode = signal(false);
ngOnInit() {
const id = this.route.snapshot.params['id'];
if (id) {
this.isEditMode = true;
this.isEditMode.set(true);
this.pageId = id;
this.loadPage(id);
} else {
this.isEditMode = false;
this.isEditMode.set(false);
this.buildForm();
}
}
private loadPage(id: string) {
this.pageService.get(id).subscribe(page => {
this.page = page;
this.page.set(page);
this.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);
this.form = new FormGroup({
...baseForm.controls,
content: new FormControl(this.page?.content || ''),
script: new FormControl(this.page?.script || ''),
style: new FormControl(this.page?.style || ''),
content: new FormControl(currentPage?.content || ''),
script: new FormControl(currentPage?.script || ''),
style: new FormControl(currentPage?.style || ''),
});
prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef);
}
private executeSaveOperation(operation: 'save' | 'draft' | 'publish') {
if (this.isEditMode) {
if (!this.page || !this.pageId) {
if (this.isEditMode()) {
const currentPage = this.page();
if (!currentPage || !this.pageId) {
return;
}
switch (operation) {
case 'save':
this.pageFormService.update(this.pageId, this.form, this.page).subscribe();
this.pageFormService.update(this.pageId, this.form, currentPage).subscribe();
break;
case 'draft':
this.pageFormService.updateAsDraft(this.pageId, this.form, this.page).subscribe();
this.pageFormService.updateAsDraft(this.pageId, this.form, currentPage).subscribe();
break;
case 'publish':
this.pageFormService.updateAndPublish(this.pageId, this.form, this.page).subscribe();
this.pageFormService.updateAndPublish(this.pageId, this.form, currentPage).subscribe();
break;
}
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-body">
<div class="row">
@ -21,6 +21,10 @@
</div>
<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>
</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 { FormsModule } from '@angular/forms';
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';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-page-list',
templateUrl: './page-list.component.html',
providers: [
@ -20,43 +22,36 @@ import { eCmsKitAdminComponents } from '../../../enums';
],
imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule],
})
export class PageListComponent implements OnInit {
data: PagedResultDto<PageDto> = { items: [], totalCount: 0 };
export class PageListComponent {
public readonly list = inject(ListService<GetPagesInputDto>);
private pageService = inject(PageAdminService);
private confirmationService = inject(ConfirmationService);
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() {
this.hookToQuery();
}
filter = '';
onSearch() {
this.list.filter = this.filter;
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) {
this.confirmationService
.warn('CmsKit::PageDeletionConfirmationMessage', 'AbpUi::AreYouSure', {
@ -65,9 +60,7 @@ export class PageListComponent implements OnInit {
})
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.pageService.delete(id).subscribe(() => {
this.list.get();
});
this.pageService.delete(id).subscribe(() => 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-body">
<div class="row">
@ -21,10 +21,10 @@
</div>
<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>
@if (isModalVisible) {
<abp-tag-modal [selected]="selected" (visibleChange)="onVisibleModalChange($event)" />
@if (isModalVisible()) {
<abp-tag-modal [selected]="selected()" (visibleChange)="onVisibleModalChange($event)" />
}
</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 { FormsModule } from '@angular/forms';
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';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-tag-list',
templateUrl: './tag-list.component.html',
providers: [
@ -28,28 +30,35 @@ import { TagModalComponent, TagModalVisibleChange } from '../tag-modal/tag-modal
TagModalComponent,
],
})
export class TagListComponent implements OnInit {
data: PagedResultDto<TagDto> = { items: [], totalCount: 0 };
export class TagListComponent {
public readonly list = inject(ListService<TagGetListInput>);
private tagService = inject(TagAdminService);
private confirmationService = inject(ConfirmationService);
filter = '';
isModalVisible = false;
selected?: TagDto;
tagDefinitions: TagDefinitionDto[] = [];
readonly data = toSignal(
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);
}),
{
initialValue: { items: [], totalCount: 0 } as PagedResultDto<TagDto>,
},
);
ngOnInit() {
this.loadTagDefinitions();
this.hookToQuery();
}
readonly tagDefinitions = toSignal(this.tagService.getTagDefinitions(), {
initialValue: [] as TagDefinitionDto[],
});
private loadTagDefinitions() {
this.tagService.getTagDefinitions().subscribe(definitions => {
this.tagDefinitions = definitions;
});
}
filter = '';
readonly isModalVisible = signal(false);
readonly selected = signal<TagDto | undefined>(undefined);
onSearch() {
this.list.filter = this.filter;
@ -57,35 +66,17 @@ export class TagListComponent implements OnInit {
}
add() {
this.selected = {} as TagDto;
this.isModalVisible = true;
this.selected.set({} as TagDto);
this.isModalVisible.set(true);
}
edit(id: string) {
this.tagService.get(id).subscribe(tag => {
this.selected = tag;
this.isModalVisible = true;
this.selected.set(tag);
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) {
if (visibilityChange.visible) {
return;
@ -93,8 +84,8 @@ export class TagListComponent implements OnInit {
if (visibilityChange.refresh) {
this.list.get();
}
this.selected = null;
this.isModalVisible = false;
this.selected.set(undefined);
this.isModalVisible.set(false);
}
delete(id: string, name: string) {
@ -104,9 +95,7 @@ export class TagListComponent implements OnInit {
})
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.tagService.delete(id).subscribe(() => {
this.list.get();
});
this.tagService.delete(id).subscribe(() => 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 { ReactiveFormsModule, FormGroup } from '@angular/forms';
import { NgxValidateCoreModule } from '@ngx-validate/core';
@ -22,6 +30,7 @@ export interface TagModalVisibleChange {
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-tag-modal',
templateUrl: './tag-modal.component.html',
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 { EditorState } from '@codemirror/state';
import { EditorView, lineNumbers } from '@codemirror/view';
import { basicSetup } from 'codemirror';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-codemirror-editor',
template: `<div #cmHost class="codemirror-container"></div>`,
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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
Component,
AfterViewInit,
ViewChild,
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
forwardRef,
inject,
PLATFORM_ID,
DestroyRef,
ViewChild,
} from '@angular/core';
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';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-toastui-editor',
template: `<div #editorContainer></div>`,
providers: [

Loading…
Cancel
Save