diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html index 4b121e5114..2f8a80d721 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html @@ -3,6 +3,30 @@
@if (form && (!isEditMode || blogPost)) {
+ +
+ @if (coverImagePreview) { +
+ Cover Image +
+ +
+
+ } + + +
+ @@ -12,6 +36,22 @@
+ + + @if (isTagsEnabled) { +
+
+ + +
{{ 'CmsKit::TagsHelpText' | abpLocalization }}
+
+ } } @else {
diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts index 4af9bc8d88..2f6e77620b 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts @@ -1,9 +1,12 @@ import { Component, OnInit, inject, Injector, DestroyRef } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms'; +import { ReactiveFormsModule, FormsModule, FormGroup, FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { NgxValidateCoreModule } from '@ngx-validate/core'; -import { LocalizationPipe } from '@abp/ng.core'; +import { forkJoin, of } from 'rxjs'; +import { switchMap, tap } from 'rxjs/operators'; +import { LocalizationPipe, RestService } from '@abp/ng.core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ExtensibleFormComponent, FormPropData, @@ -13,7 +16,15 @@ import { import { PageComponent } from '@abp/ng.components/page'; import { ButtonComponent } from '@abp/ng.theme.shared'; import { ToastuiEditorComponent, prepareSlugFromControl } from '@abp/ng.cms-kit'; -import { BlogPostAdminService, BlogPostDto } from '@abp/ng.cms-kit/proxy'; +import { + BlogPostAdminService, + BlogPostDto, + MediaDescriptorAdminService, + EntityTagAdminService, + CreateMediaInputWithStream, + TagDto, + BlogFeatureDto, +} from '@abp/ng.cms-kit/proxy'; import { eCmsKitAdminComponents } from '../../../enums'; import { BlogPostFormService } from '../../../services'; @@ -33,12 +44,16 @@ import { BlogPostFormService } from '../../../services'; ToastuiEditorComponent, LocalizationPipe, ReactiveFormsModule, + FormsModule, CommonModule, NgxValidateCoreModule, ], }) export class BlogPostFormComponent implements OnInit { private blogPostService = inject(BlogPostAdminService); + private mediaService = inject(MediaDescriptorAdminService); + private entityTagService = inject(EntityTagAdminService); + private restService = inject(RestService); private injector = inject(Injector); private blogPostFormService = inject(BlogPostFormService); private route = inject(ActivatedRoute); @@ -48,6 +63,11 @@ export class BlogPostFormComponent implements OnInit { blogPost: BlogPostDto | null = null; blogPostId: string | null = null; isEditMode = false; + coverImageFile: File | null = null; + coverImagePreview: string | null = null; + tags: string = ''; + isTagsEnabled = true; + readonly BLOG_POST_ENTITY_TYPE = 'BlogPost'; ngOnInit() { const id = this.route.snapshot.params['id']; @@ -64,43 +84,160 @@ export class BlogPostFormComponent implements OnInit { private loadBlogPost(id: string) { this.blogPostService.get(id).subscribe(blogPost => { this.blogPost = blogPost; + if (blogPost.coverImageMediaId) { + this.coverImagePreview = `/api/cms-kit/media/${blogPost.coverImageMediaId}`; + } this.buildForm(); + this.loadTags(id); }); } + private loadTags(blogPostId: string) { + this.restService + .request({ + method: 'GET', + url: `/api/cms-kit-public/tags/${this.BLOG_POST_ENTITY_TYPE}/${blogPostId}`, + }) + .subscribe(tags => { + if (tags && tags.length > 0) { + this.tags = tags.map(t => t.name || '').join(', '); + } + }); + } + private buildForm() { const data = new FormPropData(this.injector, this.blogPost || {}); const baseForm = generateFormFromProps(data); this.form = new FormGroup({ ...baseForm.controls, content: new FormControl(this.blogPost?.content || ''), + coverImageMediaId: new FormControl(this.blogPost?.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; + if (blogId) { + this.checkTagsFeature(blogId); + } + + // Listen for blog selection changes + this.form + .get('blogId') + ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(blogId => { + if (blogId) { + this.checkTagsFeature(blogId); + } + }); } - private executeSaveOperation(operation: 'save' | 'draft' | 'publish' | 'sendToReview') { - if (this.isEditMode) { - if (!this.blogPost || !this.blogPostId) { - return; - } - this.blogPostFormService.update(this.blogPostId, this.form, this.blogPost).subscribe(); - return; + private checkTagsFeature(blogId: string) { + this.restService + .request({ + method: 'GET', + url: `/api/cms-kit/blogs/${blogId}/features/CmsKit.Tags`, + }) + .subscribe(feature => { + this.isTagsEnabled = feature?.isEnabled === true; + }); + } + + onCoverImageChange(event: Event) { + const input = event.target as HTMLInputElement; + if (input.files && input.files[0]) { + this.coverImageFile = input.files[0]; + const reader = new FileReader(); + reader.onload = (e: any) => { + this.coverImagePreview = e.target.result; + }; + reader.readAsDataURL(this.coverImageFile); + } + } + + removeCoverImage() { + this.coverImageFile = null; + this.coverImagePreview = null; + this.form.patchValue({ coverImageMediaId: null }); + } + + private uploadCoverImage() { + if (!this.coverImageFile) { + return of(this.form.value.coverImageMediaId || null); + } + + const input: CreateMediaInputWithStream = { + name: this.coverImageFile.name, + file: this.coverImageFile as any, + }; + + return this.mediaService.create('blogpost', input).pipe( + tap(result => { + this.form.patchValue({ coverImageMediaId: result.id }); + }), + switchMap(result => of(result.id || null)), + ); + } + + private setTags(blogPostId: string) { + if (!this.tags || !this.tags.trim()) { + return of(null); } - switch (operation) { - case 'save': - this.blogPostFormService.createAsDraft(this.form).subscribe(); - break; - case 'draft': - this.blogPostFormService.createAsDraft(this.form).subscribe(); - break; - case 'publish': - this.blogPostFormService.createAndPublish(this.form).subscribe(); - break; - case 'sendToReview': - this.blogPostFormService.createAndSendToReview(this.form).subscribe(); - break; + const tagArray = this.tags + .split(',') + .map(t => t.trim()) + .filter(t => t.length > 0); + + if (tagArray.length === 0) { + return of(null); } + + return this.entityTagService.setEntityTags({ + entityType: this.BLOG_POST_ENTITY_TYPE, + entityId: blogPostId, + tags: tagArray, + }); + } + + private executeSaveOperation(operation: 'save' | 'draft' | 'publish' | 'sendToReview') { + // First upload cover image if selected + this.uploadCoverImage() + .pipe( + tap(coverImageMediaId => { + if (coverImageMediaId) { + this.form.patchValue({ coverImageMediaId }); + } + }), + switchMap(() => { + if (this.isEditMode) { + if (!this.blogPost || !this.blogPostId) { + return of(null); + } + return this.blogPostFormService.update(this.blogPostId, this.form, this.blogPost); + } + + switch (operation) { + case 'save': + case 'draft': + return this.blogPostFormService.createAsDraft(this.form); + case 'publish': + return this.blogPostFormService.createAndPublish(this.form); + case 'sendToReview': + return this.blogPostFormService.createAndSendToReview(this.form); + default: + return of(null); + } + }), + switchMap(result => { + if (!result || !result.id) { + return of(null); + } + // Set tags after blog post is created/updated + return forkJoin([of(result), this.setTags(result.id)]); + }), + ) + .subscribe(); } saveAsDraft() {