mirror of https://github.com/abpframework/abp.git
committed by
GitHub
190 changed files with 56792 additions and 4 deletions
File diff suppressed because it is too large
@ -0,0 +1,659 @@ |
|||
# ABP Npm Package Development Guide |
|||
|
|||
## Table of Contents |
|||
|
|||
1. [Overview](#overview) |
|||
2. [Project Structure](#project-structure) |
|||
3. [Module Architecture](#module-architecture) |
|||
4. [Component Development](#component-development) |
|||
5. [Service Layer](#service-layer) |
|||
6. [Routing & Navigation](#routing--navigation) |
|||
7. [State Management](#state-management) |
|||
8. [Form Handling](#form-handling) |
|||
9. [Validation](#validation) |
|||
10. [Testing](#testing) |
|||
11. [Best Practices](#best-practices) |
|||
12. [Common Patterns](#common-patterns) |
|||
13. [Troubleshooting](#troubleshooting) |
|||
|
|||
## Overview |
|||
|
|||
This guide provides comprehensive instructions for developing ABP modules following the established patterns and conventions. The guide is based on the SaaS module structure and can be applied to any ABP module development. |
|||
|
|||
### Key Principles |
|||
|
|||
- **Modularity**: Each module should be self-contained with clear boundaries |
|||
- **Extensibility**: Modules should support customization through extension points |
|||
- **Consistency**: Follow established ABP patterns and conventions |
|||
- **Testability**: All components should be easily testable |
|||
- **Performance**: Optimize for bundle size and runtime performance |
|||
|
|||
## Project Structure |
|||
|
|||
``` |
|||
package-name/ |
|||
├── package.json # Package metadata and dependencies |
|||
├── ng-package.json # ng-packagr configuration |
|||
├── project.json # Nx workspace configuration |
|||
├── tsconfig.json # TypeScript root config |
|||
├── tsconfig.lib.json # Library-specific TS config |
|||
├── tsconfig.lib.prod.json # Production build config |
|||
├── tsconfig.spec.json # Test configuration |
|||
├── jest.config.ts # Jest test configuration |
|||
├── tslint.json # (optional) Linting rules |
|||
├── README.md # Package documentation |
|||
│ |
|||
├── src/ # Main library source code |
|||
│ ├── lib/ # Core library implementation |
|||
│ │ ├── components/ # Angular components |
|||
│ │ ├── services/ # Business logic services |
|||
│ │ ├── models/ # TypeScript interfaces/models |
|||
│ │ ├── enums/ # Enumerations |
|||
│ │ ├── guards/ # Route guards |
|||
│ │ ├── resolvers/ # Route resolvers |
|||
│ │ ├── defaults/ # Default configurations |
|||
│ │ ├── tokens/ # Dependency injection tokens |
|||
│ │ ├── utils/ # Utility functions |
|||
│ │ ├── validators/ # Form validators |
|||
│ │ ├── [feature].routes.ts |
|||
│ └── public-api.ts # Public exports barrel file |
|||
│ |
|||
├── config/ # Configuration sub-package (optional) |
|||
│ ├── ng-package.json |
|||
│ └── src/ |
|||
│ ├── components/ # Config-specific components |
|||
│ ├── providers/ # Route/setting providers |
|||
│ ├── services/ # Config services |
|||
│ ├── models/ # Config models |
|||
│ ├── enums/ # Config enums |
|||
│ └── public-api.ts |
|||
│ |
|||
├── proxy/ # API proxy sub-package (optional) |
|||
│ ├── ng-package.json |
|||
│ └── src/ |
|||
│ ├── lib/ |
|||
│ │ └── proxy/ |
|||
│ │ ├── [feature]/ # Generated proxy services |
|||
│ │ ├── generate-proxy.json |
|||
│ │ └── README.md |
|||
│ └── public-api.ts |
|||
│ |
|||
├── common/ # Common/shared sub-package (optional) |
|||
│ ├── ng-package.json |
|||
│ └── src/ |
|||
│ ├── enums/ |
|||
│ ├── tokens/ |
|||
│ └── public-api.ts |
|||
│ |
|||
└── admin/ # Admin-specific sub-package (optional) |
|||
├── ng-package.json |
|||
└── src/ |
|||
└── ... |
|||
``` |
|||
|
|||
### File Naming Conventions |
|||
|
|||
- Use kebab-case for file names: `my-component.component.ts` |
|||
- Use PascalCase for class names: `MyComponent` |
|||
- Use camelCase for variables and methods: `myVariable`, `myMethod()` |
|||
- Use UPPER_SNAKE_CASE for constants: `MY_CONSTANT` |
|||
|
|||
## Package Architecture |
|||
|
|||
### Configuration Options Pattern |
|||
|
|||
```typescript |
|||
export interface MyConfigOptions { |
|||
entityActionContributors?: MyEntityActionContributors; |
|||
toolbarActionContributors?: MyToolbarActionContributors; |
|||
entityPropContributors?: MyEntityPropContributors; |
|||
createFormPropContributors?: MyCreateFormPropContributors; |
|||
editFormPropContributors?: MyEditFormPropContributors; |
|||
} |
|||
``` |
|||
|
|||
## Component Development |
|||
|
|||
### Component Structure |
|||
|
|||
```typescript |
|||
import { Component, OnInit, OnDestroy } from '@angular/core'; |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { MyService } from '../services'; |
|||
|
|||
@Component({ |
|||
selector: 'app-my-component', |
|||
templateUrl: './my-component.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eMyComponents.MyComponent, |
|||
}, |
|||
], |
|||
imports: [], |
|||
}) |
|||
export class MyComponent implements OnInit, OnDestroy { |
|||
// Properties |
|||
data = this.list.getGrid(); |
|||
isModalVisible = false; |
|||
|
|||
public readonly list = inject(ListService); |
|||
private myService = inject(MyService); |
|||
|
|||
ngOnInit() { |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.list.hookToQuery = () => {}; |
|||
} |
|||
|
|||
// Methods |
|||
onEdit(id: string) { |
|||
// Implementation |
|||
} |
|||
|
|||
onDelete(id: string) { |
|||
// Implementation |
|||
} |
|||
|
|||
private hookToQuery() { |
|||
this.list |
|||
.hookToQuery(query => this.myService.getList({ ...query, ...this.filters })) |
|||
.subscribe(res => (this.data = res)); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Example HTML template |
|||
|
|||
```html |
|||
<abp-page [title]="'AbpLocalizationKey::SubKey' | abpLocalization" [toolbar]="data.items"> |
|||
<div> |
|||
<div class="mt-2 mt-sm-0"> |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpLocalizationKey"> |
|||
<abp-advanced-entity-filters-form> |
|||
<form #filterForm (keyup.enter)="list.get()"> |
|||
<!-- ... --> |
|||
</form> |
|||
</abp-advanced-entity-filters-form> |
|||
</abp-advanced-entity-filters> |
|||
</div> |
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
</div> |
|||
</abp-page> |
|||
|
|||
<abp-modal [(visible)]="isModalVisible" [busy]="modalBusy" (disappear)="form = null"> |
|||
<ng-template #abpHeader> |
|||
<h3> |
|||
@if (selected?.id) { {{ 'AbpLocalizationKey::Edit' | abpLocalization }} @if |
|||
(selected.userName) { - {{ selected.userName }} } } @else { {{ 'AbpLocalizationKey::New' | |
|||
abpLocalization }} } |
|||
</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" id="myForm" (ngSubmit)="save()" validateOnSubmit> |
|||
<a ngbNavLink>{{ 'AbpLocalizationKey::MyInfo' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="row"> |
|||
<abp-extensible-form class="row gap-x2" [selectedRecord]="selected" /> |
|||
</div> |
|||
</ng-template> |
|||
</form> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" buttonType="submit" formName="myForm"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
``` |
|||
|
|||
### Component Best Practices |
|||
|
|||
1. **Single Responsibility**: Each component should have one clear purpose |
|||
2. **Dependency Injection**: Use constructor injection for services |
|||
3. **Lifecycle Management**: Implement `OnInit` and `OnDestroy` when needed |
|||
4. **State Management**: Use reactive forms and observables |
|||
5. **Error Handling**: Implement proper error boundaries |
|||
6. **Accessibility**: Follow ARIA guidelines |
|||
7. **Performance**: Use `OnPush` change detection when possible |
|||
|
|||
## Service Layer |
|||
|
|||
### Service Structure |
|||
|
|||
```typescript |
|||
import { Injectable } from '@angular/core'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import { MyDto } from '../models'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class MyService extends RestService { |
|||
protected get url() { |
|||
return 'api/my-endpoint'; |
|||
} |
|||
|
|||
getList(query: any) { |
|||
return this.request<MyDto[]>({ |
|||
method: 'GET', |
|||
params: query, |
|||
}); |
|||
} |
|||
|
|||
getById(id: string) { |
|||
return this.request<MyDto>({ |
|||
method: 'GET', |
|||
url: `${this.url}/${id}`, |
|||
}); |
|||
} |
|||
|
|||
create(input: Partial<MyDto>) { |
|||
return this.request<MyDto>({ |
|||
method: 'POST', |
|||
body: input, |
|||
}); |
|||
} |
|||
|
|||
update(id: string, input: Partial<MyDto>) { |
|||
return this.request<MyDto>({ |
|||
method: 'PUT', |
|||
url: `${this.url}/${id}`, |
|||
body: input, |
|||
}); |
|||
} |
|||
|
|||
delete(id: string) { |
|||
return this.request<void>({ |
|||
method: 'DELETE', |
|||
url: `${this.url}/${id}`, |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Service Best Practices |
|||
|
|||
1. **Extend RestService**: Use ABP's base service for HTTP operations |
|||
2. **Type Safety**: Use TypeScript interfaces for all data structures |
|||
3. **Error Handling**: Implement proper error handling and logging |
|||
4. **Caching**: Consider caching strategies for frequently accessed data |
|||
5. **Observables**: Use RxJS observables for reactive programming |
|||
|
|||
## Routing & Navigation |
|||
|
|||
### Route Configuration |
|||
|
|||
```typescript |
|||
import { Routes } from '@angular/router'; |
|||
import { Provider } from '@angular/core'; |
|||
import { |
|||
RouterOutletComponent, |
|||
authGuard, |
|||
permissionGuard, |
|||
ReplaceableRouteContainerComponent, |
|||
ReplaceableComponents, |
|||
} from '@abp/ng.core'; |
|||
|
|||
export function createRoutes(config: MyConfigOptions = {}): Routes { |
|||
return [ |
|||
{ path: '', redirectTo: 'my-feature', pathMatch: 'full' }, |
|||
{ |
|||
path: '', |
|||
component: RouterOutletComponent, |
|||
providers: provideMyContributors(config), |
|||
canActivate: [authGuard, permissionGuard], |
|||
children: [ |
|||
{ |
|||
path: 'my-feature', |
|||
component: ReplaceableRouteContainerComponent, |
|||
data: { |
|||
requiredPolicy: 'My.Feature', |
|||
replaceableComponent: { |
|||
key: eMyComponents.MyFeature, |
|||
defaultComponent: MyFeatureComponent, |
|||
} as ReplaceableComponents.RouteData<MyFeatureComponent>, |
|||
}, |
|||
title: 'My::Feature', |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
function provideMyContributors(options: MyConfigOptions = {}): Provider[] { |
|||
return [ |
|||
// ... providers |
|||
]; |
|||
} |
|||
``` |
|||
|
|||
### Route Best Practices |
|||
|
|||
1. **Lazy Loading**: Use lazy loading for better performance |
|||
2. **Guards**: Implement authentication and authorization guards |
|||
3. **Permissions**: Use permission-based route protection |
|||
4. **Replaceable Components**: Support component replacement for extensibility |
|||
5. **SEO**: Use meaningful route titles and metadata |
|||
|
|||
## State Management |
|||
|
|||
### State Management Patterns |
|||
|
|||
```typescript |
|||
// Using ABP's ConfigStateService |
|||
import { ConfigStateService } from '@abp/ng.core'; |
|||
|
|||
export class MyComponent { |
|||
private configState = inject(ConfigStateService); |
|||
|
|||
getSettings() { |
|||
return this.configState.getSetting('My.Setting'); |
|||
} |
|||
} |
|||
|
|||
// Using reactive forms |
|||
import { FormBuilder, FormGroup } from '@angular/forms'; |
|||
|
|||
export class MyComponent { |
|||
form: FormGroup; |
|||
private fb = inject(FormBuilder); |
|||
|
|||
constructor() { |
|||
this.form = this.fb.group({ |
|||
name: ['', Validators.required], |
|||
email: ['', [Validators.required, Validators.email]], |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Form Handling |
|||
|
|||
### Form Structure |
|||
|
|||
```typescript |
|||
import { Component } from '@angular/core'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { MyService } from '../services'; |
|||
|
|||
@Component({ |
|||
selector: 'app-my-form', |
|||
template: ` |
|||
<form [formGroup]="form" (ngSubmit)="onSubmit()"> |
|||
<input formControlName="name" /> |
|||
<input formControlName="email" /> |
|||
<button type="submit">Submit</button> |
|||
</form> |
|||
`, |
|||
imports: [], |
|||
}) |
|||
export class MyFormComponent { |
|||
form: FormGroup; |
|||
|
|||
private fb = inject(FormBuilder); |
|||
private myService = inject(MyService); |
|||
|
|||
constructor() { |
|||
this.form = this.fb.group({ |
|||
name: ['', Validators.required], |
|||
email: ['', [Validators.required, Validators.email]], |
|||
}); |
|||
} |
|||
|
|||
onSubmit() { |
|||
if (this.form.valid) { |
|||
this.myService.create(this.form.value).subscribe( |
|||
result => { |
|||
// Handle success |
|||
}, |
|||
error => { |
|||
// Handle error |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Validation |
|||
|
|||
### Custom Validators |
|||
|
|||
```typescript |
|||
import { Provider } from '@angular/core'; |
|||
import { AsyncValidatorFn, FormGroup } from '@angular/forms'; |
|||
import { of } from 'rxjs'; |
|||
|
|||
export const MY_VALIDATOR_PROVIDER: Provider = { |
|||
provide: MY_FORM_ASYNC_VALIDATORS_TOKEN, |
|||
multi: true, |
|||
useFactory: myCustomValidator, |
|||
}; |
|||
|
|||
export function myCustomValidator(): AsyncValidatorFn { |
|||
return (group: FormGroup) => { |
|||
// Validation logic |
|||
const field1 = group?.get('field1'); |
|||
const field2 = group?.get('field2'); |
|||
|
|||
if (!field1 || !field2) { |
|||
return of(null); |
|||
} |
|||
|
|||
if (field1.value && !field2.value) { |
|||
field2.setErrors({ required: true }); |
|||
} |
|||
|
|||
return of(null); |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
### Validation Best Practices |
|||
|
|||
1. **Async Validators**: Use for server-side validation |
|||
2. **Cross-field Validation**: Validate relationships between fields |
|||
3. **Error Messages**: Provide clear, user-friendly error messages |
|||
4. **Performance**: Debounce async validators to avoid excessive API calls |
|||
5. **Accessibility**: Ensure validation errors are announced to screen readers |
|||
|
|||
## Testing |
|||
|
|||
### Unit Testing Structure |
|||
|
|||
```typescript |
|||
import { ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
import { MyComponent } from './my.component'; |
|||
import { MyService } from '../services'; |
|||
|
|||
describe('MyComponent', () => { |
|||
let component: MyComponent; |
|||
let fixture: ComponentFixture<MyComponent>; |
|||
let myService: jasmine.SpyObj<MyService>; |
|||
|
|||
beforeEach(async () => { |
|||
const spy = jasmine.createSpyObj('MyService', ['getList']); |
|||
|
|||
await TestBed.configureTestingModule({ |
|||
declarations: [MyComponent], |
|||
providers: [{ provide: MyService, useValue: spy }], |
|||
}).compileComponents(); |
|||
|
|||
fixture = TestBed.createComponent(MyComponent); |
|||
component = fixture.componentInstance; |
|||
myService = TestBed.inject(MyService) as jasmine.SpyObj<MyService>; |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should load data on init', () => { |
|||
// Test implementation |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
### Testing Best Practices |
|||
|
|||
1. **Isolation**: Test components in isolation |
|||
2. **Mocking**: Mock external dependencies |
|||
3. **Coverage**: Aim for high test coverage |
|||
4. **Integration Tests**: Test component interactions |
|||
5. **E2E Tests**: Test complete user workflows |
|||
|
|||
## Best Practices |
|||
|
|||
### Code Organization |
|||
|
|||
1. **Feature-based Structure**: Organize code by features, not types |
|||
2. **Barrel Exports**: Use index files for clean imports |
|||
3. **Consistent Naming**: Follow established naming conventions |
|||
4. **Documentation**: Document complex logic and public APIs |
|||
5. **Type Safety**: Use TypeScript strictly |
|||
|
|||
### Performance |
|||
|
|||
1. **Lazy Loading**: Load modules on demand |
|||
2. **Change Detection**: Use OnPush strategy when possible |
|||
3. **Memory Management**: Unsubscribe from observables |
|||
4. **Bundle Size**: Minimize bundle size through tree shaking |
|||
5. **Caching**: Implement appropriate caching strategies |
|||
|
|||
### Security |
|||
|
|||
1. **Input Validation**: Validate all user inputs |
|||
2. **XSS Prevention**: Sanitize user-generated content |
|||
3. **CSRF Protection**: Use CSRF tokens for state-changing operations |
|||
4. **Authorization**: Check permissions at component and service levels |
|||
5. **HTTPS**: Use HTTPS in production |
|||
|
|||
## Common Patterns |
|||
|
|||
### Extension Pattern |
|||
|
|||
```typescript |
|||
// Define extension tokens |
|||
export const MY_ENTITY_ACTION_CONTRIBUTORS = new InjectionToken<EntityActionContributors>( |
|||
'MY_ENTITY_ACTION_CONTRIBUTORS' |
|||
); |
|||
|
|||
// Provide default implementations |
|||
export const DEFAULT_MY_ENTITY_ACTIONS = { |
|||
[eMyComponents.MyFeature]: DEFAULT_MY_FEATURE_ACTIONS, |
|||
}; |
|||
|
|||
// Use in module |
|||
{ |
|||
provide: MY_ENTITY_ACTION_CONTRIBUTORS, |
|||
useValue: options.entityActionContributors || DEFAULT_MY_ENTITY_ACTIONS, |
|||
} |
|||
``` |
|||
|
|||
### Modal Pattern |
|||
|
|||
```typescript |
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class MyModalService { |
|||
private modalRef: NgbModalRef; |
|||
|
|||
private modalService = inject(NgbModal); |
|||
|
|||
show(data?: any): NgbModalRef { |
|||
this.modalRef = this.modalService.open(MyModalComponent, { |
|||
size: 'lg', |
|||
backdrop: 'static', |
|||
}); |
|||
|
|||
if (data) { |
|||
this.modalRef.componentInstance.data = data; |
|||
} |
|||
|
|||
return this.modalRef; |
|||
} |
|||
|
|||
close() { |
|||
if (this.modalRef) { |
|||
this.modalRef.close(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### List Pattern |
|||
|
|||
```typescript |
|||
export class MyListComponent { |
|||
data = this.list.getGrid(); |
|||
|
|||
readonly list = inject(ListService); |
|||
|
|||
ngOnInit() { |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
private hookToQuery() { |
|||
this.list |
|||
.hookToQuery(query => this.pageService.getList({ ...query, ...this.filters })) |
|||
.subscribe(res => (this.data = res)); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Common Issues |
|||
|
|||
1. **Module Not Found**: Check import paths and module declarations |
|||
2. **Circular Dependencies**: Use forwardRef() or restructure imports |
|||
3. **Memory Leaks**: Ensure proper cleanup in ngOnDestroy |
|||
4. **Performance Issues**: Use OnPush change detection and memoization |
|||
5. **Type Errors**: Ensure proper TypeScript configuration |
|||
|
|||
### Debugging Tips |
|||
|
|||
1. **Angular DevTools**: Use Angular DevTools for component inspection |
|||
2. **Console Logging**: Use console.log strategically for debugging |
|||
3. **Network Tab**: Monitor API calls in browser dev tools |
|||
4. **Error Boundaries**: Implement error boundaries for graceful error handling |
|||
5. **Unit Tests**: Use tests to reproduce and fix bugs |
|||
|
|||
### Performance Optimization |
|||
|
|||
1. **Bundle Analysis**: Use webpack-bundle-analyzer to identify large dependencies |
|||
2. **Lazy Loading**: Implement route-based code splitting |
|||
3. **Tree Shaking**: Ensure unused code is eliminated |
|||
4. **Caching**: Implement appropriate caching strategies |
|||
5. **Minification**: Ensure proper minification in production builds |
|||
|
|||
--- |
|||
|
|||
## Conclusion |
|||
|
|||
This guide provides a comprehensive overview of ABP module development patterns and best practices. Follow these guidelines to create maintainable, extensible, and performant modules that integrate seamlessly with the ABP framework. |
|||
|
|||
Remember to: |
|||
|
|||
- Follow established ABP conventions |
|||
- Write comprehensive tests |
|||
- Document your code |
|||
- Consider performance implications |
|||
- Implement proper error handling |
|||
- Use TypeScript features effectively |
|||
|
|||
For more information, refer to the official ABP documentation and community resources. |
|||
@ -0,0 +1,452 @@ |
|||
# ABP Package Development - Quick Reference |
|||
|
|||
## Essential Commands |
|||
|
|||
### Build & Test |
|||
|
|||
```bash |
|||
# Open a terminal on ng-packs directory |
|||
cd /Users/sumeyyekurtulus/Desktop/volosoft/GITHUB/abp/npm/ng-packs |
|||
|
|||
# Build the package |
|||
yarn nx build package-name --skip-nx-cache |
|||
|
|||
# Run tests |
|||
yarn nx test package-name --test-file test-file.spec.ts |
|||
|
|||
# Lint code |
|||
yarn run lint |
|||
|
|||
# Build for production |
|||
yarn nx build package-name --configuration=production |
|||
``` |
|||
|
|||
### Development |
|||
|
|||
```bash |
|||
# Open a terminal on ng-packs directory |
|||
cd /Users/sumeyyekurtulus/Desktop/volosoft/GITHUB/abp/npm/ng-packs |
|||
|
|||
# Start development server |
|||
yarn start |
|||
|
|||
# Watch for changes |
|||
yarn run watch |
|||
|
|||
# Generate component |
|||
ng generate component my-component |
|||
|
|||
# Generate service |
|||
ng generate service my-service |
|||
``` |
|||
|
|||
## File Structure Quick Reference |
|||
|
|||
``` |
|||
package-name/ |
|||
├── package.json # Package metadata and dependencies |
|||
├── ng-package.json # ng-packagr configuration |
|||
├── project.json # Nx workspace configuration |
|||
├── tsconfig.json # TypeScript root config |
|||
├── tsconfig.lib.json # Library-specific TS config |
|||
├── tsconfig.lib.prod.json # Production build config |
|||
├── tsconfig.spec.json # Test configuration |
|||
├── jest.config.ts # Jest test configuration |
|||
├── tslint.json # (optional) Linting rules |
|||
├── README.md # Package documentation |
|||
│ |
|||
├── src/ # Main library source code |
|||
│ ├── lib/ # Core library implementation |
|||
│ │ ├── components/ # Angular components |
|||
│ │ ├── services/ # Business logic services |
|||
│ │ ├── models/ # TypeScript interfaces/models |
|||
│ │ ├── enums/ # Enumerations |
|||
│ │ ├── guards/ # Route guards |
|||
│ │ ├── resolvers/ # Route resolvers |
|||
│ │ ├── defaults/ # Default configurations |
|||
│ │ ├── tokens/ # Dependency injection tokens |
|||
│ │ ├── utils/ # Utility functions |
|||
│ │ ├── validators/ # Form validators |
|||
│ │ ├── [feature].routes.ts |
|||
│ └── public-api.ts # Public exports barrel file |
|||
│ |
|||
├── config/ # Configuration sub-package (optional) |
|||
│ ├── ng-package.json |
|||
│ └── src/ |
|||
│ ├── components/ # Config-specific components |
|||
│ ├── providers/ # Route/setting providers |
|||
│ ├── services/ # Config services |
|||
│ ├── models/ # Config models |
|||
│ ├── enums/ # Config enums |
|||
│ └── public-api.ts |
|||
│ |
|||
├── proxy/ # API proxy sub-package (optional) [Do not touch while making generation] |
|||
│ ├── ng-package.json |
|||
│ └── src/ |
|||
│ ├── lib/ |
|||
│ │ └── proxy/ |
|||
│ │ ├── [feature]/ # Generated proxy services |
|||
│ │ ├── generate-proxy.json |
|||
│ │ └── README.md |
|||
│ └── public-api.ts |
|||
│ |
|||
├── common/ # Common/shared sub-package (optional) |
|||
│ ├── ng-package.json |
|||
│ └── src/ |
|||
│ ├── enums/ |
|||
│ ├── tokens/ |
|||
│ └── public-api.ts |
|||
│ |
|||
└── admin/ # Admin-specific sub-package (optional) |
|||
├── ng-package.json |
|||
└── src/ |
|||
└── ... |
|||
``` |
|||
|
|||
## Common Patterns |
|||
|
|||
### Component Definition |
|||
|
|||
```typescript |
|||
@Component({ |
|||
selector: 'app-my-component', |
|||
templateUrl: './my-component.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eMyComponents.MyComponent, |
|||
}, |
|||
], |
|||
imports: [], |
|||
}) |
|||
export class MyComponent implements OnInit { |
|||
public readonly list = inject(ListService); |
|||
private myComponentService = inject(MyComponentService); |
|||
|
|||
data = this.list.getGrid(); |
|||
|
|||
ngOnInit() { |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
private hookToQuery() { |
|||
this.list |
|||
.hookToQuery(query => this.myComponentService.getList({ ...query, ...this.filters })) |
|||
.subscribe(res => (this.data = res)); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Default Extension Points |
|||
|
|||
```typescript |
|||
export const DEFAULT_MY_ENTITY_ACTIONS = EntityAction.createMany<MyElementDto>([ |
|||
{ |
|||
text: 'AbpPackage::MyElement', |
|||
action: data => { |
|||
const { piece } = data.record; |
|||
if (!piece) { |
|||
return; |
|||
} |
|||
|
|||
const router = data.getInjected(Router); |
|||
router.navigate(['/package/piece']); |
|||
}, |
|||
permission: 'AbpPackage.MyElement', |
|||
}, |
|||
]); |
|||
``` |
|||
|
|||
```typescript |
|||
export const DEFAULT_MY_ENTITY_PROPS = EntityProp.createMany<MyElementDto>([ |
|||
{ |
|||
type: ePropType.PropType, |
|||
name: 'propName', |
|||
displayName: 'AbpPackage::PropDisplayName', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
]); |
|||
``` |
|||
|
|||
```typescript |
|||
export const DEFAULT_MY_CREATE_FORM_PROPS = FormProp.createMany<MyElementDto>([ |
|||
{ |
|||
type: ePropType.PropType, |
|||
name: 'propName', |
|||
displayName: 'AbpPackage::PropDisplayName', |
|||
id: 'propID', |
|||
}, |
|||
]); |
|||
export const DEFAULT_MY_EDIT_FORM_PROPS = DEFAULT_MY_CREATE_FORM_PROPS.filter( |
|||
prop => prop.name !== 'propName', |
|||
); |
|||
``` |
|||
|
|||
```typescript |
|||
export const DEFAULT_MY_TOOLBAR_COMPONENTS = ToolbarComponent.createMany<MyElementDto[]>([ |
|||
{ |
|||
permission: 'AbpPermissionKey', |
|||
component: MyComponent, |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_MY_TOOLBAR_ACTIONS = ToolbarAction.createMany<MyElementDto[]>([ |
|||
{ |
|||
text: 'AbpPackage::Text', |
|||
action: data => { |
|||
const component = data.getInjected(MyComponent); |
|||
component.onAdd(); |
|||
}, |
|||
permission: 'AbpPermissionKey', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_USERS_TOOLBAR_ALL = [ |
|||
...DEFAULT_USERS_TOOLBAR_COMPONENTS, |
|||
...DEFAULT_USERS_TOOLBAR_ACTIONS, |
|||
]; |
|||
``` |
|||
|
|||
### Route Definition |
|||
|
|||
```typescript |
|||
export function createRoutes(config: MyConfigOptions = {}): Routes { |
|||
return [ |
|||
{ |
|||
path: '', |
|||
component: RouterOutletComponent, |
|||
providers: provideMyContributors(config), |
|||
canActivate: [authGuard, permissionGuard], |
|||
children: [ |
|||
{ |
|||
path: 'my-feature', |
|||
component: ReplaceableRouteContainerComponent, |
|||
data: { |
|||
requiredPolicy: 'My.Feature', |
|||
replaceableComponent: { |
|||
key: eMyComponents.MyFeature, |
|||
defaultComponent: MyFeatureComponent, |
|||
}, |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
function provideMyContributors(options: MyConfigOptions = {}): Provider[] { |
|||
return [ |
|||
{ |
|||
provide: MY_ENTITY_ACTION_CONTRIBUTORS, |
|||
useValue: options.entityActionContributors, |
|||
}, |
|||
{ |
|||
provide: MY_TOOLBAR_ACTION_CONTRIBUTORS, |
|||
useValue: options.toolbarActionContributors, |
|||
}, |
|||
{ |
|||
provide: MY_ENTITY_PROP_CONTRIBUTORS, |
|||
useValue: options.entityPropContributors, |
|||
}, |
|||
{ |
|||
provide: MY_CREATE_FORM_PROP_CONTRIBUTORS, |
|||
useValue: options.createFormPropContributors, |
|||
}, |
|||
{ |
|||
provide: MY_EDIT_FORM_PROP_CONTRIBUTORS, |
|||
useValue: options.editFormPropContributors, |
|||
}, |
|||
]; |
|||
} |
|||
``` |
|||
|
|||
```typescript |
|||
export const APP_ROUTES: Routes = [ |
|||
{ |
|||
path: 'my-route', |
|||
loadChildren: () => import('my-package').then(c => c.createRoutes()), |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
### Provider Definitions |
|||
|
|||
```typescript |
|||
export const MY_ROUTE_PROVIDERS = [ |
|||
provideAppInitializer(() => { |
|||
configureRoutes(); |
|||
}), |
|||
]; |
|||
|
|||
export function configureRoutes() { |
|||
const routes = inject(RoutesService); |
|||
routes.add([ |
|||
{ |
|||
path: '/my-route', |
|||
name: eMyRouteNames.Route, |
|||
parentName: eThemeSharedRouteNames.Route, |
|||
order: 2, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-id-card-o', |
|||
requiredPolicy: eMyPolicyNames.Route, |
|||
}, |
|||
{ |
|||
path: '/my-route/my-sub-route', |
|||
name: eMyRouteNames.MySubRoute, |
|||
parentName: eMyRouteNames.Route, |
|||
order: 1, |
|||
requiredPolicy: eMyPolicyNames.MySubRoute, |
|||
}, |
|||
]); |
|||
} |
|||
``` |
|||
|
|||
```typescript |
|||
export function provideMyConfig() { |
|||
return makeEnvironmentProviders([MY_ROUTE_PROVIDERS]); |
|||
} |
|||
``` |
|||
|
|||
```typescript |
|||
export const appConfig: ApplicationConfig = { |
|||
providers: [provideMyConfig()], |
|||
}; |
|||
``` |
|||
|
|||
## Validation Patterns |
|||
|
|||
### Custom Validator |
|||
|
|||
```typescript |
|||
export const MY_VALIDATOR_PROVIDER: Provider = { |
|||
provide: MY_FORM_ASYNC_VALIDATORS_TOKEN, |
|||
multi: true, |
|||
useFactory: myCustomValidator, |
|||
}; |
|||
|
|||
export function myCustomValidator(): AsyncValidatorFn { |
|||
return (group: FormGroup) => { |
|||
// Validation logic |
|||
return of(null); |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
## Testing Patterns |
|||
|
|||
### Component Test |
|||
|
|||
```typescript |
|||
describe('MyComponent', () => { |
|||
let component: MyComponent; |
|||
let fixture: ComponentFixture<MyComponent>; |
|||
|
|||
beforeEach(async () => { |
|||
await TestBed.configureTestingModule({ |
|||
declarations: [MyComponent], |
|||
providers: [{ provide: MyService, useValue: jasmine.createSpyObj('MyService', ['getList']) }], |
|||
}).compileComponents(); |
|||
|
|||
fixture = TestBed.createComponent(MyComponent); |
|||
component = fixture.componentInstance; |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
## Configuration Options |
|||
|
|||
### Module Configuration |
|||
|
|||
```typescript |
|||
export interface MyConfigOptions { |
|||
entityActionContributors?: MyEntityActionContributors; |
|||
toolbarActionContributors?: MyToolbarActionContributors; |
|||
entityPropContributors?: MyEntityPropContributors; |
|||
createFormPropContributors?: MyCreateFormPropContributors; |
|||
editFormPropContributors?: MyEditFormPropContributors; |
|||
} |
|||
``` |
|||
|
|||
## Common Imports |
|||
|
|||
### Common Services |
|||
|
|||
```typescript |
|||
import { ListService, ConfigStateService } from '@abp/ng.core'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import { EntityAction, FormProp } from '@abp/ng.components/extensible'; |
|||
``` |
|||
|
|||
### Common Guards & Components |
|||
|
|||
```typescript |
|||
import { authGuard, permissionGuard } from '@abp/ng.core'; |
|||
import { RouterOutletComponent, ReplaceableRouteContainerComponent } from '@abp/ng.core'; |
|||
``` |
|||
|
|||
## Naming Conventions |
|||
|
|||
| Type | Convention | Example | |
|||
| ----------------- | -------------------------- | --------------------------- | |
|||
| Files | kebab-case | `my-component.component.ts` | |
|||
| Classes | PascalCase | `MyComponent` | |
|||
| Variables/Methods | camelCase | `myVariable`, `myMethod()` | |
|||
| Constants | UPPER_SNAKE_CASE | `MY_CONSTANT` | |
|||
| Interfaces | PascalCase with 'I' prefix | `IMyInterface` | |
|||
| Enums | PascalCase with 'e' prefix | `eMyEnum` | |
|||
|
|||
## Best Practices Checklist |
|||
|
|||
- [ ] Follow single responsibility principle |
|||
- [ ] Use dependency injection |
|||
- [ ] Implement proper error handling |
|||
- [ ] Write unit tests |
|||
- [ ] Use TypeScript strictly |
|||
- [ ] Follow ABP conventions |
|||
- [ ] Document public APIs |
|||
- [ ] Optimize for performance |
|||
- [ ] Implement proper validation |
|||
- [ ] Use reactive forms |
|||
- [ ] Handle lifecycle properly |
|||
- [ ] Implement proper security |
|||
|
|||
## Common Issues & Solutions |
|||
|
|||
### Module Not Found |
|||
|
|||
- Check import paths |
|||
- Verify module declarations |
|||
- Ensure proper exports |
|||
|
|||
### Circular Dependencies |
|||
|
|||
- Use `forwardRef()` |
|||
- Restructure imports |
|||
- Move shared code to separate modules |
|||
|
|||
### Memory Leaks |
|||
|
|||
- Unsubscribe from observables |
|||
- Implement `OnDestroy` |
|||
- Use `takeUntil` operator |
|||
|
|||
### Performance Issues |
|||
|
|||
- Use `OnPush` change detection |
|||
- Implement lazy loading |
|||
- Optimize bundle size |
|||
|
|||
## Useful Resources |
|||
|
|||
- [ABP Documentation](https://docs.abp.io) |
|||
- [Angular Documentation](https://angular.io/docs) |
|||
- [TypeScript Handbook](https://www.typescriptlang.org/docs) |
|||
- [RxJS Documentation](https://rxjs.dev) |
|||
- [ABP Community](https://community.abp.io) |
|||
@ -0,0 +1,85 @@ |
|||
# @abp/ng.cms-kit |
|||
|
|||
ABP CMS Kit Angular package providing admin and public functionality for content management. |
|||
|
|||
## Structure |
|||
|
|||
This package is organized into two main sub-packages: |
|||
|
|||
- **Admin** (`admin/`) - Admin interface for managing CMS content |
|||
- **Public** (`public/`) - Public-facing components for displaying CMS content |
|||
|
|||
## Installation |
|||
|
|||
```bash |
|||
npm install @abp/ng.cms-kit |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
### Admin |
|||
|
|||
```typescript |
|||
import { provideCmsKitAdminConfig } from '@abp/ng.cms-kit/admin/config'; |
|||
|
|||
// In your app config |
|||
export const appConfig: ApplicationConfig = { |
|||
providers: [ |
|||
provideCmsKitAdminConfig(), |
|||
// ... other providers |
|||
], |
|||
}; |
|||
|
|||
// In your routes |
|||
export const routes: Routes = [ |
|||
{ |
|||
path: 'cms', |
|||
loadChildren: () => import('@abp/ng.cms-kit/admin').then(m => m.createRoutes()), |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
### Public |
|||
|
|||
```typescript |
|||
import { provideCmsKitPublicConfig } from '@abp/ng.cms-kit/public/config'; |
|||
|
|||
// In your app config |
|||
export const appConfig: ApplicationConfig = { |
|||
providers: [ |
|||
provideCmsKitPublicConfig(), |
|||
// ... other providers |
|||
], |
|||
}; |
|||
|
|||
// In your routes |
|||
export const routes: Routes = [ |
|||
{ |
|||
path: 'cms', |
|||
loadChildren: () => import('@abp/ng.cms-kit/public').then(m => m.createRoutes()), |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
## Features |
|||
|
|||
### Admin Features |
|||
|
|||
- Comments management |
|||
- Tags management |
|||
- Pages management |
|||
- Blogs management |
|||
- Blog posts management |
|||
- Menus management |
|||
- Global resources management |
|||
|
|||
### Public Features |
|||
|
|||
- Public page viewing |
|||
- Public blog and blog post viewing |
|||
- Commenting functionality |
|||
- Shared components (MarkedItemToggle, PopularTags, Rating, ReactionSelection, Tags) |
|||
|
|||
## Documentation |
|||
|
|||
For more information, see the [ABP Documentation](https://docs.abp.io). |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "../../../../node_modules/ng-packagr/ng-entrypoint.schema.json", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export * from './policy-names'; |
|||
export * from './route-names'; |
|||
@ -0,0 +1,10 @@ |
|||
export enum eCmsKitAdminPolicyNames { |
|||
Cms = 'CmsKit.Comments || CmsKit.Tags || CmsKit.Pages || CmsKit.Blogs || CmsKit.BlogPosts || CmsKit.Menus || CmsKit.GlobalResources', |
|||
Comments = 'CmsKit.Comments', |
|||
Tags = 'CmsKit.Tags', |
|||
Pages = 'CmsKit.Pages', |
|||
Blogs = 'CmsKit.Blogs', |
|||
BlogPosts = 'CmsKit.BlogPosts', |
|||
Menus = 'CmsKit.Menus', |
|||
GlobalResources = 'CmsKit.GlobalResources', |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
export enum eCmsKitAdminRouteNames { |
|||
Cms = 'CmsKit::Menu:CMS', |
|||
Comments = 'CmsKit::CmsKit.Comments', |
|||
Tags = 'CmsKit::CmsKit.Tags', |
|||
Pages = 'CmsKit::Pages', |
|||
Blogs = 'CmsKit::Blogs', |
|||
BlogPosts = 'CmsKit::BlogPosts', |
|||
Menus = 'CmsKit::Menus', |
|||
GlobalResources = 'CmsKit::GlobalResources', |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export interface Settings { |
|||
commentRequireApprovement: boolean; |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './cms-kit-admin-settings'; |
|||
@ -0,0 +1,10 @@ |
|||
import { makeEnvironmentProviders } from '@angular/core'; |
|||
import { CMS_KIT_ADMIN_ROUTE_PROVIDERS } from './route.provider'; |
|||
import { CMS_KIT_ADMIN_SETTING_TAB_PROVIDERS } from './setting-tab.provider'; |
|||
|
|||
export function provideCmsKitAdminConfig() { |
|||
return makeEnvironmentProviders([ |
|||
CMS_KIT_ADMIN_ROUTE_PROVIDERS, |
|||
CMS_KIT_ADMIN_SETTING_TAB_PROVIDERS, |
|||
]); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './cms-kit-admin-config.provider'; |
|||
export * from './route.provider'; |
|||
export * from './setting-tab.provider'; |
|||
@ -0,0 +1,79 @@ |
|||
import { eLayoutType, RoutesService } from '@abp/ng.core'; |
|||
import { inject, provideAppInitializer } from '@angular/core'; |
|||
import { eCmsKitAdminPolicyNames } from '../enums/policy-names'; |
|||
import { eCmsKitAdminRouteNames } from '../enums/route-names'; |
|||
|
|||
export const CMS_KIT_ADMIN_ROUTE_PROVIDERS = [ |
|||
provideAppInitializer(() => { |
|||
configureRoutes(); |
|||
}), |
|||
]; |
|||
|
|||
export function configureRoutes() { |
|||
const routesService = inject(RoutesService); |
|||
routesService.add([ |
|||
{ |
|||
path: '/cms/blog-posts', |
|||
name: eCmsKitAdminRouteNames.BlogPosts, |
|||
parentName: eCmsKitAdminRouteNames.Cms, |
|||
order: 1, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-file-alt', |
|||
requiredPolicy: eCmsKitAdminPolicyNames.BlogPosts, |
|||
}, |
|||
{ |
|||
path: '/cms/blogs', |
|||
name: eCmsKitAdminRouteNames.Blogs, |
|||
parentName: eCmsKitAdminRouteNames.Cms, |
|||
order: 2, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-blog', |
|||
requiredPolicy: eCmsKitAdminPolicyNames.Blogs, |
|||
}, |
|||
{ |
|||
path: '/cms/comments', |
|||
name: eCmsKitAdminRouteNames.Comments, |
|||
parentName: eCmsKitAdminRouteNames.Cms, |
|||
order: 3, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-comments', |
|||
requiredPolicy: eCmsKitAdminPolicyNames.Comments, |
|||
}, |
|||
{ |
|||
path: '/cms/global-resources', |
|||
name: eCmsKitAdminRouteNames.GlobalResources, |
|||
parentName: eCmsKitAdminRouteNames.Cms, |
|||
order: 5, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-globe', |
|||
requiredPolicy: eCmsKitAdminPolicyNames.GlobalResources, |
|||
}, |
|||
{ |
|||
path: '/cms/menus', |
|||
name: eCmsKitAdminRouteNames.Menus, |
|||
parentName: eCmsKitAdminRouteNames.Cms, |
|||
order: 6, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-bars', |
|||
requiredPolicy: eCmsKitAdminPolicyNames.Menus, |
|||
}, |
|||
{ |
|||
path: '/cms/pages', |
|||
name: eCmsKitAdminRouteNames.Pages, |
|||
parentName: eCmsKitAdminRouteNames.Cms, |
|||
order: 9, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-file', |
|||
requiredPolicy: eCmsKitAdminPolicyNames.Pages, |
|||
}, |
|||
{ |
|||
path: '/cms/tags', |
|||
name: eCmsKitAdminRouteNames.Tags, |
|||
parentName: eCmsKitAdminRouteNames.Cms, |
|||
order: 11, |
|||
layout: eLayoutType.application, |
|||
iconClass: 'fa fa-tags', |
|||
requiredPolicy: eCmsKitAdminPolicyNames.Tags, |
|||
}, |
|||
]); |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import { ABP, ConfigStateService } from '@abp/ng.core'; |
|||
import { SettingTabsService } from '@abp/ng.setting-management/config'; |
|||
import { inject, provideAppInitializer } from '@angular/core'; |
|||
import { eCmsKitAdminPolicyNames, eCmsKitAdminRouteNames } from '../enums'; |
|||
import { CmsSettingsComponent } from '@abp/ng.cms-kit/admin'; |
|||
|
|||
export const CMS_KIT_ADMIN_SETTING_TAB_PROVIDERS = [ |
|||
provideAppInitializer(() => { |
|||
configureSettingTabs(); |
|||
}), |
|||
]; |
|||
|
|||
export async function configureSettingTabs() { |
|||
const settingTabs = inject(SettingTabsService); |
|||
const configState = inject(ConfigStateService); |
|||
const tabsArray: ABP.Tab[] = [ |
|||
{ |
|||
name: eCmsKitAdminRouteNames.Cms, |
|||
order: 100, |
|||
requiredPolicy: eCmsKitAdminPolicyNames.Cms, |
|||
invisible: configState.getFeature('CmsKit.CommentEnable')?.toLowerCase() === 'true', |
|||
component: CmsSettingsComponent, |
|||
}, |
|||
]; |
|||
|
|||
settingTabs.add(tabsArray); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './enums'; |
|||
export * from './providers'; |
|||
export * from './models'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,241 @@ |
|||
import { Routes } from '@angular/router'; |
|||
import { Provider } from '@angular/core'; |
|||
import { |
|||
RouterOutletComponent, |
|||
authGuard, |
|||
permissionGuard, |
|||
ReplaceableRouteContainerComponent, |
|||
} from '@abp/ng.core'; |
|||
import { eCmsKitAdminComponents } from './enums'; |
|||
import { |
|||
CommentListComponent, |
|||
CommentDetailsComponent, |
|||
TagListComponent, |
|||
PageListComponent, |
|||
PageFormComponent, |
|||
BlogListComponent, |
|||
BlogPostListComponent, |
|||
BlogPostFormComponent, |
|||
MenuItemListComponent, |
|||
GlobalResourcesComponent, |
|||
} from './components'; |
|||
import { |
|||
CMS_KIT_ADMIN_ENTITY_ACTION_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_ENTITY_PROP_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_TOOLBAR_ACTION_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_CREATE_FORM_PROP_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_EDIT_FORM_PROP_CONTRIBUTORS, |
|||
} from './tokens'; |
|||
import { cmsKitAdminExtensionsResolver } from './resolvers'; |
|||
import { CmsKitAdminConfigOptions } from './models'; |
|||
|
|||
export function createRoutes(config: CmsKitAdminConfigOptions = {}): Routes { |
|||
return [ |
|||
{ |
|||
path: '', |
|||
component: RouterOutletComponent, |
|||
providers: provideCmsKitAdminContributors(config), |
|||
canActivate: [authGuard, permissionGuard], |
|||
children: [ |
|||
{ |
|||
path: 'comments', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Comments', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.CommentList, |
|||
defaultComponent: CommentListComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::Comments', |
|||
}, |
|||
{ |
|||
path: 'comments/:id', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Comments', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.CommentDetails, |
|||
defaultComponent: CommentDetailsComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::Comments', |
|||
}, |
|||
{ |
|||
path: 'tags', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Tags', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.Tags, |
|||
defaultComponent: TagListComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::Tags', |
|||
}, |
|||
{ |
|||
path: 'pages', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Pages', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.Pages, |
|||
defaultComponent: PageListComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::Pages', |
|||
}, |
|||
{ |
|||
path: 'pages/create', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Pages.Create', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.PageForm, |
|||
defaultComponent: PageFormComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::Pages', |
|||
}, |
|||
{ |
|||
path: 'pages/update/:id', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Pages.Update', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.PageForm, |
|||
defaultComponent: PageFormComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::Pages', |
|||
}, |
|||
{ |
|||
path: 'blogs', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Blogs', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.Blogs, |
|||
defaultComponent: BlogListComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::Blogs', |
|||
}, |
|||
{ |
|||
path: 'blog-posts', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.BlogPosts', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.BlogPosts, |
|||
defaultComponent: BlogPostListComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::BlogPosts', |
|||
}, |
|||
{ |
|||
path: 'blog-posts/create', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.BlogPosts.Create', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.BlogPostForm, |
|||
defaultComponent: BlogPostFormComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::BlogPosts', |
|||
}, |
|||
{ |
|||
path: 'blog-posts/update/:id', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.BlogPosts.Update', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.BlogPostForm, |
|||
defaultComponent: BlogPostFormComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::BlogPosts', |
|||
}, |
|||
{ |
|||
path: 'menus', |
|||
component: ReplaceableRouteContainerComponent, |
|||
resolve: { |
|||
extensions: cmsKitAdminExtensionsResolver, |
|||
}, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.Menus', |
|||
replaceableComponent: { |
|||
key: eCmsKitAdminComponents.Menus, |
|||
defaultComponent: MenuItemListComponent, |
|||
}, |
|||
}, |
|||
title: 'CmsKit::MenuItems', |
|||
}, |
|||
{ |
|||
path: 'global-resources', |
|||
component: GlobalResourcesComponent, |
|||
data: { |
|||
requiredPolicy: 'CmsKit.GlobalResources', |
|||
}, |
|||
title: 'CmsKit::GlobalResources', |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
function provideCmsKitAdminContributors(options: CmsKitAdminConfigOptions = {}): Provider[] { |
|||
return [ |
|||
{ |
|||
provide: CMS_KIT_ADMIN_ENTITY_ACTION_CONTRIBUTORS, |
|||
useValue: options.entityActionContributors, |
|||
}, |
|||
{ |
|||
provide: CMS_KIT_ADMIN_ENTITY_PROP_CONTRIBUTORS, |
|||
useValue: options.entityPropContributors, |
|||
}, |
|||
{ |
|||
provide: CMS_KIT_ADMIN_TOOLBAR_ACTION_CONTRIBUTORS, |
|||
useValue: options.toolbarActionContributors, |
|||
}, |
|||
{ |
|||
provide: CMS_KIT_ADMIN_CREATE_FORM_PROP_CONTRIBUTORS, |
|||
useValue: options.createFormPropContributors, |
|||
}, |
|||
{ |
|||
provide: CMS_KIT_ADMIN_EDIT_FORM_PROP_CONTRIBUTORS, |
|||
useValue: options.editFormPropContributors, |
|||
}, |
|||
]; |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
<abp-page [title]="'CmsKit::BlogPosts' | abpLocalization"> |
|||
<div class="card"> |
|||
<div class="card-body"> |
|||
@if (form && (!isEditMode || blogPost)) { |
|||
<form [formGroup]="form" (ngSubmit)="saveAsDraft()" validateOnSubmit> |
|||
<!-- Cover Image --> |
|||
<div class="mb-3"> |
|||
@if (coverImagePreview) { |
|||
<div class="mb-2"> |
|||
<img [src]="coverImagePreview" height="120" alt="Cover Image" /> |
|||
<br /> |
|||
<button type="button" class="btn btn-link p-0" (click)="removeCoverImage()"> |
|||
{{ 'CmsKit::RemoveCoverImage' | abpLocalization }} |
|||
</button> |
|||
<br /> |
|||
</div> |
|||
} |
|||
<label class="form-label" |
|||
>{{ 'CmsKit::CoverImage' | abpLocalization }} |
|||
<span class="badge text-bg-light">16:9</span></label |
|||
> |
|||
<input |
|||
type="file" |
|||
class="form-control" |
|||
accept="image/*" |
|||
(change)="onCoverImageChange($event)" |
|||
/> |
|||
</div> |
|||
|
|||
<!-- Basic fields --> |
|||
<abp-extensible-form [selectedRecord]="blogPost || {}" /> |
|||
|
|||
<!-- Content editor --> |
|||
<div class="mt-3"> |
|||
<label class="form-label">{{ 'CmsKit::Content' | abpLocalization }}</label> |
|||
<abp-toastui-editor formControlName="content" /> |
|||
</div> |
|||
</form> |
|||
|
|||
<!-- Tags Editor --> |
|||
@if (isTagsEnabled) { |
|||
<hr /> |
|||
<div class="mb-3"> |
|||
<label class="form-label">{{ 'CmsKit::Tags' | abpLocalization }}</label> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
[(ngModel)]="tags" |
|||
[ngModelOptions]="{ standalone: true }" |
|||
[placeholder]="'CmsKit::TagsHelpText' | abpLocalization" |
|||
/> |
|||
<div class="form-text">{{ 'CmsKit::TagsHelpText' | abpLocalization }}</div> |
|||
</div> |
|||
} |
|||
} @else { |
|||
<div class="text-center"> |
|||
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i> |
|||
</div> |
|||
} |
|||
</div> |
|||
<div class="card-footer"> |
|||
<div class="d-flex justify-content-start gap-2"> |
|||
<button class="btn btn-secondary" (click)="saveAsDraft()" [disabled]="form?.invalid"> |
|||
{{ 'CmsKit::SaveAsDraft' | abpLocalization }} |
|||
</button> |
|||
@if (isEditMode) { |
|||
<abp-button (click)="publish()" [disabled]="form?.invalid"> |
|||
{{ 'CmsKit::Publish' | abpLocalization }} |
|||
</abp-button> |
|||
} @else { |
|||
<abp-button (click)="publish()" [disabled]="form?.invalid"> |
|||
{{ 'CmsKit::Publish' | abpLocalization }} |
|||
</abp-button> |
|||
<abp-button (click)="sendToReview()" [disabled]="form?.invalid"> |
|||
{{ 'CmsKit::SendToReviewToPublish' | abpLocalization }} |
|||
</abp-button> |
|||
} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</abp-page> |
|||
@ -0,0 +1,257 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ActivatedRoute } from '@angular/router'; |
|||
import { Component, OnInit, inject, Injector, DestroyRef } from '@angular/core'; |
|||
import { ReactiveFormsModule, FormsModule, FormGroup, FormControl } from '@angular/forms'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { forkJoin, of } from 'rxjs'; |
|||
import { switchMap, tap } from 'rxjs/operators'; |
|||
import { LocalizationPipe, RestService } from '@abp/ng.core'; |
|||
import { |
|||
ExtensibleFormComponent, |
|||
FormPropData, |
|||
generateFormFromProps, |
|||
EXTENSIONS_IDENTIFIER, |
|||
} from '@abp/ng.components/extensible'; |
|||
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, |
|||
MediaDescriptorAdminService, |
|||
EntityTagAdminService, |
|||
CreateMediaInputWithStream, |
|||
TagDto, |
|||
BlogFeatureDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { BlogPostFormService } from '../../../services'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-post-form', |
|||
templateUrl: './blog-post-form.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.BlogPostForm, |
|||
}, |
|||
], |
|||
imports: [ |
|||
ButtonComponent, |
|||
ExtensibleFormComponent, |
|||
PageComponent, |
|||
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); |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
form: FormGroup; |
|||
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']; |
|||
if (id) { |
|||
this.isEditMode = true; |
|||
this.blogPostId = id; |
|||
this.loadBlogPost(id); |
|||
} else { |
|||
this.isEditMode = false; |
|||
this.buildForm(); |
|||
} |
|||
} |
|||
|
|||
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) { |
|||
// TODO: use the public service to load the tags
|
|||
this.restService |
|||
.request<void, TagDto[]>({ |
|||
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 checkTagsFeature(blogId: string) { |
|||
this.restService |
|||
.request<void, BlogFeatureDto>({ |
|||
method: 'GET', |
|||
url: `/api/cms-kit/blogs/${blogId}/features/CmsKit.Tags`, |
|||
}) |
|||
.subscribe(feature => { |
|||
const { isEnabled } = feature || {}; |
|||
this.isTagsEnabled = isEnabled; |
|||
}); |
|||
} |
|||
|
|||
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); |
|||
} |
|||
|
|||
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() { |
|||
this.executeSaveOperation('draft'); |
|||
} |
|||
|
|||
publish() { |
|||
this.executeSaveOperation('publish'); |
|||
} |
|||
|
|||
sendToReview() { |
|||
this.executeSaveOperation('sendToReview'); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<abp-page [title]="'CmsKit::BlogPosts' | abpLocalization" [toolbar]="data.items"> |
|||
<div class="card mb-4"> |
|||
<div class="card-body"> |
|||
<div class="row"> |
|||
<div class="col-2"> |
|||
<select class="form-select" [(ngModel)]="statusFilter" (change)="onStatusChange()"> |
|||
<option [ngValue]="null">{{ 'CmsKit::AllPosts' | abpLocalization }}</option> |
|||
<option [ngValue]="BlogPostStatus.Draft"> |
|||
{{ 'CmsKit::' + 'CmsKit.BlogPost.Status.0' | abpLocalization }} |
|||
</option> |
|||
<option [ngValue]="BlogPostStatus.Published"> |
|||
{{ 'CmsKit::' + 'CmsKit.BlogPost.Status.1' | abpLocalization }} |
|||
</option> |
|||
<option [ngValue]="BlogPostStatus.WaitingForReview"> |
|||
{{ 'CmsKit::' + 'CmsKit.BlogPost.Status.2' | abpLocalization }} |
|||
</option> |
|||
</select> |
|||
</div> |
|||
<div class="col-10"> |
|||
<div class="input-group"> |
|||
<input |
|||
type="search" |
|||
class="form-control" |
|||
[placeholder]="'AbpUi::PagerSearch' | abpLocalization" |
|||
[(ngModel)]="filter" |
|||
(keyup.enter)="onSearch()" |
|||
/> |
|||
<button class="btn btn-primary" type="button" (click)="onSearch()"> |
|||
<i class="fa fa-search" aria-hidden="true"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
</abp-page> |
|||
@ -0,0 +1,86 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; |
|||
import { ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { |
|||
BlogPostAdminService, |
|||
BlogPostGetListInput, |
|||
BlogPostListDto, |
|||
BlogPostStatus, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-post-list', |
|||
templateUrl: './blog-post-list.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.BlogPosts, |
|||
}, |
|||
], |
|||
imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule], |
|||
}) |
|||
export class BlogPostListComponent implements OnInit { |
|||
data: PagedResultDto<BlogPostListDto> = { items: [], totalCount: 0 }; |
|||
|
|||
public readonly list = inject(ListService<BlogPostGetListInput>); |
|||
private blogPostService = inject(BlogPostAdminService); |
|||
private confirmationService = inject(ConfirmationService); |
|||
|
|||
filter = ''; |
|||
statusFilter: BlogPostStatus | null = null; |
|||
BlogPostStatus = BlogPostStatus; |
|||
|
|||
ngOnInit() { |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
onSearch() { |
|||
this.list.filter = this.filter; |
|||
this.list.get(); |
|||
} |
|||
|
|||
onStatusChange() { |
|||
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', { |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
messageLocalizationParams: [title], |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.blogPostService.delete(id).subscribe(() => { |
|||
this.list.get(); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export * from './blog-post-list/blog-post-list.component'; |
|||
export * from './blog-post-form/blog-post-form.component'; |
|||
@ -0,0 +1,65 @@ |
|||
<abp-modal [visible]="true" (visibleChange)="onVisibleChange($event)"> |
|||
<ng-template #abpHeader> |
|||
<h3>{{ 'CmsKit::Features' | abpLocalization }}</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<div formArrayName="features"> |
|||
@for (featureControl of featuresFormArray.controls; track $index; let i = $index) { |
|||
<div class="mb-3" [formGroupName]="i"> |
|||
@let isAvailable = featureControl.get('isAvailable')?.value; |
|||
@if (isAvailable) { |
|||
<div class="form-check"> |
|||
<input |
|||
type="checkbox" |
|||
class="form-check-input" |
|||
id="feature-{{ i }}" |
|||
formControlName="isEnabled" |
|||
/> |
|||
<label class="form-check-label" for="feature-{{ i }}"> |
|||
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }} |
|||
</label> |
|||
</div> |
|||
} @else { |
|||
<div |
|||
data-toggle="tooltip" |
|||
[title]="'CmsKit::BlogFeatureNotAvailable' | abpLocalization" |
|||
> |
|||
<div class="form-check"> |
|||
<input |
|||
type="checkbox" |
|||
class="form-check-input" |
|||
id="feature-{{ i }}" |
|||
formControlName="isEnabled" |
|||
disabled |
|||
/> |
|||
<label class="form-check-label" for="feature-{{ i }}"> |
|||
{{ 'CmsKit::' + features[i]?.featureName | abpLocalization }} |
|||
</label> |
|||
</div> |
|||
</div> |
|||
} |
|||
<input type="hidden" formControlName="featureName" /> |
|||
<input type="hidden" formControlName="isAvailable" /> |
|||
</div> |
|||
} |
|||
</div> |
|||
</form> |
|||
} @else { |
|||
<div class="text-center"> |
|||
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i> |
|||
</div> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,134 @@ |
|||
import { Component, OnInit, inject, input, output } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormArray, FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { forkJoin } from 'rxjs'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { |
|||
BlogFeatureAdminService, |
|||
BlogFeatureDto, |
|||
BlogFeatureInputDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
export interface BlogFeaturesModalVisibleChange { |
|||
visible: boolean; |
|||
refresh: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-features-modal', |
|||
templateUrl: './blog-features-modal.component.html', |
|||
imports: [ |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
], |
|||
}) |
|||
export class BlogFeaturesModalComponent implements OnInit { |
|||
private blogFeatureService = inject(BlogFeatureAdminService); |
|||
private fb = inject(FormBuilder); |
|||
private toasterService = inject(ToasterService); |
|||
|
|||
blogId = input<string>(); |
|||
visibleChange = output<BlogFeaturesModalVisibleChange>(); |
|||
|
|||
form: FormGroup; |
|||
features: BlogFeatureDto[] = []; |
|||
private initialFeatureStates: Map<string, boolean> = new Map(); |
|||
|
|||
ngOnInit() { |
|||
if (this.blogId()) { |
|||
this.loadFeatures(); |
|||
} |
|||
} |
|||
|
|||
private loadFeatures() { |
|||
this.blogFeatureService.getList(this.blogId()!).subscribe(features => { |
|||
this.features = features.sort((a, b) => |
|||
(a.featureName || '').localeCompare(b.featureName || ''), |
|||
); |
|||
// Store initial states
|
|||
this.initialFeatureStates = new Map( |
|||
this.features.map(f => [f.featureName || '', f.isEnabled || false]), |
|||
); |
|||
this.buildForm(); |
|||
}); |
|||
} |
|||
|
|||
private buildForm() { |
|||
const featureControls = this.features.map(feature => |
|||
this.fb.group({ |
|||
featureName: [feature.featureName], |
|||
isEnabled: [feature.isEnabled], |
|||
isAvailable: [(feature as any).isAvailable ?? true], |
|||
}), |
|||
); |
|||
|
|||
this.form = this.fb.group({ |
|||
features: this.fb.array(featureControls), |
|||
}); |
|||
} |
|||
|
|||
get featuresFormArray(): FormArray { |
|||
return this.form.get('features') as FormArray; |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean, refresh = false) { |
|||
this.visibleChange.emit({ visible, refresh }); |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid || !this.blogId()) { |
|||
return; |
|||
} |
|||
|
|||
const featuresArray = this.form.get('features') as FormArray; |
|||
|
|||
// Only save features that have changed
|
|||
const changedFeatures: BlogFeatureInputDto[] = featuresArray.controls |
|||
.map(control => { |
|||
const featureName = control.get('featureName')?.value; |
|||
const isEnabled = control.get('isEnabled')?.value; |
|||
const initialIsEnabled = this.initialFeatureStates.get(featureName); |
|||
|
|||
// Only include if the value has changed
|
|||
if (featureName && initialIsEnabled !== isEnabled) { |
|||
return { |
|||
featureName, |
|||
isEnabled, |
|||
}; |
|||
} |
|||
return null; |
|||
}) |
|||
.filter((input): input is BlogFeatureInputDto => input !== null); |
|||
|
|||
// If no features changed, just close the modal
|
|||
if (changedFeatures.length === 0) { |
|||
this.onVisibleChange(false, false); |
|||
return; |
|||
} |
|||
|
|||
// Save only changed features
|
|||
const saveObservables = changedFeatures.map(input => |
|||
this.blogFeatureService.set(this.blogId()!, input), |
|||
); |
|||
|
|||
// Use forkJoin to save all changed features at once
|
|||
forkJoin(saveObservables).subscribe({ |
|||
next: () => { |
|||
this.onVisibleChange(false, true); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<abp-page [title]="'CmsKit::Blogs' | abpLocalization" [toolbar]="data.items"> |
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
|
|||
@if (isModalVisible) { |
|||
<abp-blog-modal [selected]="selected" (visibleChange)="onVisibleModalChange($event)" /> |
|||
} |
|||
|
|||
@if (isFeaturesModalVisible) { |
|||
<abp-blog-features-modal |
|||
[blogId]="selectedBlogId" |
|||
(visibleChange)="onFeaturesModalChange($event)" |
|||
/> |
|||
} |
|||
</abp-page> |
|||
@ -0,0 +1,128 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; |
|||
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; |
|||
import { ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { BlogAdminService, BlogGetListInput, BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { BlogModalComponent, BlogModalVisibleChange } from '../blog-modal/blog-modal.component'; |
|||
import { |
|||
BlogFeaturesModalComponent, |
|||
BlogFeaturesModalVisibleChange, |
|||
} from '../blog-features-modal/blog-features-modal.component'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-list', |
|||
templateUrl: './blog-list.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.Blogs, |
|||
}, |
|||
], |
|||
imports: [ |
|||
ExtensibleTableComponent, |
|||
PageComponent, |
|||
LocalizationPipe, |
|||
FormsModule, |
|||
CommonModule, |
|||
BlogModalComponent, |
|||
BlogFeaturesModalComponent, |
|||
], |
|||
}) |
|||
export class BlogListComponent implements OnInit { |
|||
data: PagedResultDto<BlogDto> = { items: [], totalCount: 0 }; |
|||
|
|||
public readonly list = inject(ListService<BlogGetListInput>); |
|||
private blogService = inject(BlogAdminService); |
|||
private confirmationService = inject(ConfirmationService); |
|||
|
|||
filter = ''; |
|||
isModalVisible = false; |
|||
selected?: BlogDto; |
|||
isFeaturesModalVisible = false; |
|||
selectedBlogId?: string; |
|||
|
|||
ngOnInit() { |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
onSearch() { |
|||
this.list.filter = this.filter; |
|||
this.list.get(); |
|||
} |
|||
|
|||
add() { |
|||
this.selected = {} as BlogDto; |
|||
this.isModalVisible = true; |
|||
} |
|||
|
|||
edit(id: string) { |
|||
this.blogService.get(id).subscribe(blog => { |
|||
this.selected = blog; |
|||
this.isModalVisible = true; |
|||
}); |
|||
} |
|||
|
|||
delete(id: string, name: string) { |
|||
this.confirmationService |
|||
.warn('CmsKit::BlogDeletionConfirmationMessage', 'AbpUi::AreYouSure', { |
|||
messageLocalizationParams: [name], |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
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; |
|||
}); |
|||
} |
|||
|
|||
onVisibleModalChange(visibilityChange: BlogModalVisibleChange) { |
|||
if (visibilityChange.visible) { |
|||
return; |
|||
} |
|||
if (visibilityChange.refresh) { |
|||
this.list.get(); |
|||
} |
|||
this.selected = null; |
|||
this.isModalVisible = false; |
|||
} |
|||
|
|||
onFeaturesModalChange(visibilityChange: BlogFeaturesModalVisibleChange) { |
|||
if (visibilityChange.visible) { |
|||
return; |
|||
} |
|||
if (visibilityChange.refresh) { |
|||
this.list.get(); |
|||
} |
|||
this.selectedBlogId = null; |
|||
this.isFeaturesModalVisible = false; |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<abp-modal [visible]="true" (visibleChange)="onVisibleChange($event)"> |
|||
<ng-template #abpHeader> |
|||
<h3> |
|||
{{ (selected()?.id ? 'AbpUi::Edit' : 'AbpUi::New') | abpLocalization }} |
|||
</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<abp-extensible-form [selectedRecord]="selected()" /> |
|||
</form> |
|||
} @else { |
|||
<div class="text-center"> |
|||
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i> |
|||
</div> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,86 @@ |
|||
import { Component, OnInit, inject, Injector, input, output, DestroyRef } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormGroup } from '@angular/forms'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ExtensibleFormComponent, |
|||
FormPropData, |
|||
generateFormFromProps, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { BlogAdminService, BlogDto, CreateBlogDto, UpdateBlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { prepareSlugFromControl } from '@abp/ng.cms-kit'; |
|||
|
|||
export interface BlogModalVisibleChange { |
|||
visible: boolean; |
|||
refresh: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-blog-modal', |
|||
templateUrl: './blog-modal.component.html', |
|||
imports: [ |
|||
ExtensibleFormComponent, |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
], |
|||
}) |
|||
export class BlogModalComponent implements OnInit { |
|||
private blogService = inject(BlogAdminService); |
|||
private injector = inject(Injector); |
|||
private toasterService = inject(ToasterService); |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
selected = input<BlogDto>(); |
|||
visibleChange = output<BlogModalVisibleChange>(); |
|||
|
|||
form: FormGroup; |
|||
|
|||
ngOnInit() { |
|||
this.buildForm(); |
|||
} |
|||
|
|||
private buildForm() { |
|||
const data = new FormPropData(this.injector, this.selected()); |
|||
this.form = generateFormFromProps(data); |
|||
prepareSlugFromControl(this.form, 'name', 'slug', this.destroyRef); |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean, refresh = false) { |
|||
this.visibleChange.emit({ visible, refresh }); |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid) { |
|||
return; |
|||
} |
|||
|
|||
let observable$ = this.blogService.create(this.form.value as CreateBlogDto); |
|||
|
|||
const selectedBlog = this.selected(); |
|||
const { id } = selectedBlog || {}; |
|||
|
|||
if (id) { |
|||
observable$ = this.blogService.update(id, { |
|||
...selectedBlog, |
|||
...this.form.value, |
|||
} as UpdateBlogDto); |
|||
} |
|||
|
|||
observable$.subscribe(() => { |
|||
this.onVisibleChange(false, true); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './blog-list/blog-list.component'; |
|||
export * from './blog-modal/blog-modal.component'; |
|||
export * from './blog-features-modal/blog-features-modal.component'; |
|||
@ -0,0 +1,42 @@ |
|||
<div id="CmsSettingsForm" class="row"> |
|||
<div class="col-md-12"> |
|||
<ul ngbNav #nav="ngbNav" class="nav-tabs"> |
|||
<!-- Comments --> |
|||
<li ngbNavItem> |
|||
<a ngbNavLink>{{ 'CmsKit::CmsKit:Comment' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="abp-md-form"> |
|||
<div (keyup.enter)="submit()"> |
|||
<div class="mt-3"> |
|||
<div class="form-check mb-2"> |
|||
<input |
|||
class="form-check-input" |
|||
[formControl]="commentApprovalControl" |
|||
type="checkbox" |
|||
id="EnableCommentApproval" |
|||
[checked]="commentApprovalControl.value" |
|||
(input)="commentApprovalControl.setValue($event.target.checked)" |
|||
/> |
|||
<label class="form-check-label" for="EnableCommentApproval"> |
|||
{{ 'CmsKit::CmsKitCommentOptions:RequireApprovement' | abpLocalization }} |
|||
</label> |
|||
<br /> |
|||
<small class="form-text text-muted">{{ |
|||
'CmsKit::CmsKitCommentOptions:RequireApprovementDescription' | abpLocalization |
|||
}}</small> |
|||
</div> |
|||
</div> |
|||
<hr class="my-3" /> |
|||
<div> |
|||
<abp-button (click)="submit()" iconClass="fa fa-save" [loading]="loading"> |
|||
{{ 'AbpAccount::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
</ul> |
|||
<div [ngbNavOutlet]="nav"></div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,59 @@ |
|||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; |
|||
import { FormControl, ReactiveFormsModule } from '@angular/forms'; |
|||
import { |
|||
NgbNav, |
|||
NgbNavItem, |
|||
NgbNavItemRole, |
|||
NgbNavLink, |
|||
NgbNavLinkBase, |
|||
NgbNavContent, |
|||
NgbNavOutlet, |
|||
} from '@ng-bootstrap/ng-bootstrap'; |
|||
import { finalize } from 'rxjs/operators'; |
|||
import { ConfigStateService, LocalizationPipe } from '@abp/ng.core'; |
|||
import { ButtonComponent, ToasterService } from '@abp/ng.theme.shared'; |
|||
import { CommentAdminService } from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
import { CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT } from '../comments'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-cms-settings', |
|||
templateUrl: './cms-settings.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
imports: [ |
|||
NgbNav, |
|||
NgbNavItem, |
|||
NgbNavItemRole, |
|||
NgbNavLink, |
|||
NgbNavLinkBase, |
|||
NgbNavContent, |
|||
NgbNavOutlet, |
|||
LocalizationPipe, |
|||
ButtonComponent, |
|||
ReactiveFormsModule, |
|||
], |
|||
}) |
|||
export class CmsSettingsComponent { |
|||
readonly commentAdminService = inject(CommentAdminService); |
|||
readonly configState = inject(ConfigStateService); |
|||
readonly toaster = inject(ToasterService); |
|||
|
|||
commentApprovalControl = new FormControl(false); |
|||
|
|||
ngOnInit() { |
|||
const isCommentApprovalEnabled = |
|||
this.configState.getSetting(CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT).toLowerCase() === 'true'; |
|||
this.commentApprovalControl.setValue(isCommentApprovalEnabled); |
|||
} |
|||
|
|||
submit() { |
|||
this.commentAdminService |
|||
.updateSettings({ commentRequireApprovement: this.commentApprovalControl.value }) |
|||
.pipe( |
|||
finalize(() => { |
|||
this.configState.refreshAppState().subscribe(); |
|||
}), |
|||
) |
|||
.subscribe(() => this.toaster.success('AbpUi::SavedSuccessfully', null)); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './cms-settings.component'; |
|||
@ -0,0 +1,147 @@ |
|||
<abp-page [title]="'CmsKit::Comments' | abpLocalization"> |
|||
@if (comment) { |
|||
<div class="card mb-4"> |
|||
<div class="card-body"> |
|||
<table class="table"> |
|||
<tr> |
|||
<td width="10%"> |
|||
<b>{{ 'CmsKit::EntityType' | abpLocalization }}</b |
|||
>: |
|||
</td> |
|||
<td>{{ comment.entityType }}</td> |
|||
</tr> |
|||
<tr> |
|||
<td> |
|||
<b>{{ 'CmsKit::EntityId' | abpLocalization }}</b |
|||
>: |
|||
</td> |
|||
<td>{{ comment.entityId }}</td> |
|||
</tr> |
|||
<tr> |
|||
<td> |
|||
<b>{{ 'AbpIdentity::CreationTime' | abpLocalization }}</b |
|||
>: |
|||
</td> |
|||
<td>{{ comment.creationTime | date }}</td> |
|||
</tr> |
|||
<tr> |
|||
<td> |
|||
<b>{{ 'CmsKit::Username' | abpLocalization }}</b |
|||
>: |
|||
</td> |
|||
<td>{{ comment.author?.name }}</td> |
|||
</tr> |
|||
@if (comment.repliedCommentId) { |
|||
<tr> |
|||
<td> |
|||
<b>{{ 'CmsKit::ReplyTo' | abpLocalization }}</b |
|||
>: |
|||
</td> |
|||
<td> |
|||
<a (click)="navigateToReply(comment.repliedCommentId!)" style="cursor: pointer"> |
|||
{{ comment.repliedCommentId }} |
|||
</a> |
|||
</td> |
|||
</tr> |
|||
} |
|||
<tr> |
|||
<td class="align-text-top"> |
|||
<b>{{ 'CmsKit::Text' | abpLocalization }}</b |
|||
>: |
|||
</td> |
|||
<td>{{ comment.text }}</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
<div class="card mb-4"> |
|||
<div class="card-body"> |
|||
<form [formGroup]="filterForm" (ngSubmit)="onFilter()"> |
|||
<input type="hidden" [value]="commentId" /> |
|||
<div class="row"> |
|||
<div class="col-lg-4 col-md-12"> |
|||
<div class="row"> |
|||
<div class="col-lg-6 col-md-6"> |
|||
<div class="mb-3"> |
|||
<label class="form-label" for="creationStartDate"> |
|||
{{ 'CmsKit::StartDate' | abpLocalization }} |
|||
</label> |
|||
<input |
|||
id="creationStartDate" |
|||
formControlName="creationStartDate" |
|||
(click)="startDatePicker.open()" |
|||
(keyup.space)="startDatePicker.open()" |
|||
ngbDatepicker |
|||
#startDatePicker="ngbDatepicker" |
|||
type="text" |
|||
class="form-control" |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-6 col-md-6"> |
|||
<div class="mb-3"> |
|||
<label class="form-label" for="creationEndDate"> |
|||
{{ 'CmsKit::EndDate' | abpLocalization }} |
|||
</label> |
|||
<input |
|||
id="creationEndDate" |
|||
formControlName="creationEndDate" |
|||
class="form-control" |
|||
ngbDatepicker |
|||
#endDatePicker="ngbDatepicker" |
|||
(click)="endDatePicker.toggle()" |
|||
readonly |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 col-md-12"> |
|||
<abp-form-input |
|||
formControlName="author" |
|||
[label]="'CmsKit::Username' | abpLocalization" |
|||
type="text" |
|||
/> |
|||
</div> |
|||
@if (requireApprovement) { |
|||
<div class="col-lg-2 col-md-12"> |
|||
<div class="mb-3"> |
|||
<label class="mb-1 form-label" for="commentApproveState"> |
|||
{{ 'CmsKit::CommentFilter:ApproveState' | abpLocalization }} |
|||
</label> |
|||
<select |
|||
id="commentApproveState" |
|||
formControlName="commentApproveState" |
|||
class="form-control" |
|||
> |
|||
@for (option of commentApproveStateOptions; track option.value) { |
|||
<option [value]="option.value"> |
|||
{{ 'CmsKit::CommentFilter:' + option.value | abpLocalization }} |
|||
</option> |
|||
} |
|||
</select> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
<div class="col-lg-2 col-md-12"> |
|||
<div class="d-grid gap-2"> |
|||
<abp-button class="mt-md-4" buttonType="Primary" type="submit"> |
|||
<i class="fa fa-search" aria-hidden="true"></i> |
|||
</abp-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
|
|||
<h3>{{ 'CmsKit::RepliesToThisComment' | abpLocalization }}</h3> |
|||
|
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
</abp-page> |
|||
@ -0,0 +1,124 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule, DatePipe } from '@angular/common'; |
|||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; |
|||
import { ActivatedRoute, Router } from '@angular/router'; |
|||
import { NgbDateAdapter, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { ListService, LocalizationPipe, PagedResultDto } from '@abp/ng.core'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { ButtonComponent, DateTimeAdapter, FormInputComponent } from '@abp/ng.theme.shared'; |
|||
import { |
|||
CommentAdminService, |
|||
CommentGetListInput, |
|||
CommentWithAuthorDto, |
|||
CommentApproveState, |
|||
commentApproveStateOptions, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { CommentEntityService } from '../../../services'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-comment-details', |
|||
templateUrl: './comment-details.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.CommentDetails, |
|||
}, |
|||
], |
|||
viewProviders: [ |
|||
{ |
|||
provide: NgbDateAdapter, |
|||
useClass: DateTimeAdapter, |
|||
}, |
|||
], |
|||
imports: [ |
|||
ExtensibleTableComponent, |
|||
PageComponent, |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
NgbDatepickerModule, |
|||
CommonModule, |
|||
DatePipe, |
|||
FormInputComponent, |
|||
ButtonComponent, |
|||
], |
|||
}) |
|||
export class CommentDetailsComponent implements OnInit { |
|||
comment: CommentWithAuthorDto | null = null; |
|||
data: PagedResultDto<CommentWithAuthorDto> = { items: [], totalCount: 0 }; |
|||
|
|||
readonly list = inject(ListService<CommentGetListInput>); |
|||
readonly commentEntityService = inject(CommentEntityService); |
|||
|
|||
private commentService = inject(CommentAdminService); |
|||
private route = inject(ActivatedRoute); |
|||
private router = inject(Router); |
|||
private fb = inject(FormBuilder); |
|||
|
|||
filterForm!: FormGroup; |
|||
commentApproveStateOptions = commentApproveStateOptions; |
|||
commentId!: string; |
|||
requireApprovement: boolean; |
|||
|
|||
ngOnInit() { |
|||
this.route.params.subscribe(params => { |
|||
const id = params['id']; |
|||
if (id) { |
|||
this.commentId = id; |
|||
this.loadComment(id); |
|||
this.createFilterForm(); |
|||
this.hookToQuery(); |
|||
} |
|||
}); |
|||
this.requireApprovement = this.commentEntityService.requireApprovement; |
|||
} |
|||
|
|||
private createFilterForm() { |
|||
this.filterForm = this.fb.group({ |
|||
creationStartDate: [null], |
|||
creationEndDate: [null], |
|||
author: [''], |
|||
commentApproveState: [CommentApproveState.All], |
|||
}); |
|||
} |
|||
|
|||
private loadComment(id: string) { |
|||
this.commentService.get(id).subscribe(comment => { |
|||
this.comment = comment; |
|||
}); |
|||
} |
|||
|
|||
onFilter() { |
|||
const formValue = this.filterForm.value; |
|||
const filters: Partial<CommentGetListInput> = { |
|||
author: formValue.author || undefined, |
|||
commentApproveState: formValue.commentApproveState, |
|||
repliedCommentId: this.commentId, |
|||
creationStartDate: formValue.creationStartDate || undefined, |
|||
creationEndDate: formValue.creationEndDate || undefined, |
|||
}; |
|||
|
|||
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 = { |
|||
repliedCommentId: this.commentId, |
|||
...query, |
|||
...filters, |
|||
}; |
|||
return this.commentService.getList(input); |
|||
}) |
|||
.subscribe(res => (this.data = res)); |
|||
} |
|||
|
|||
navigateToReply(id: string) { |
|||
this.router.navigate(['/cms/comments', id]); |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
<abp-page [title]="'CmsKit::Comments' | abpLocalization"> |
|||
<div class="card mb-4"> |
|||
<div class="card-body"> |
|||
<form [formGroup]="filterForm" (ngSubmit)="onFilter()"> |
|||
<div class="row align-items-end"> |
|||
<div class="col-lg-4 col-md-12"> |
|||
<div class="row"> |
|||
<div class="col-lg-6 col-md-6"> |
|||
<div class="mb-3"> |
|||
<label class="form-label" for="creationStartDate"> |
|||
{{ 'CmsKit::StartDate' | abpLocalization }} |
|||
</label> |
|||
<input |
|||
id="creationStartDate" |
|||
formControlName="creationStartDate" |
|||
(click)="startDatePicker.open()" |
|||
(keyup.space)="startDatePicker.open()" |
|||
ngbDatepicker |
|||
#startDatePicker="ngbDatepicker" |
|||
type="text" |
|||
class="form-control" |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-6 col-md-6"> |
|||
<div class="mb-3"> |
|||
<label class="form-label" for="creationEndDate"> |
|||
{{ 'CmsKit::EndDate' | abpLocalization }} |
|||
</label> |
|||
<input |
|||
id="creationEndDate" |
|||
formControlName="creationEndDate" |
|||
class="form-control" |
|||
ngbDatepicker |
|||
#endDatePicker="ngbDatepicker" |
|||
(click)="endDatePicker.toggle()" |
|||
readonly |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-2 col-md-6"> |
|||
<abp-form-input |
|||
formControlName="author" |
|||
[label]="'CmsKit::Username' | abpLocalization" |
|||
type="text" |
|||
/> |
|||
</div> |
|||
|
|||
<div class="col-lg-2 col-md-6"> |
|||
<abp-form-input |
|||
formControlName="entityType" |
|||
[label]="'CmsKit::EntityType' | abpLocalization" |
|||
type="text" |
|||
/> |
|||
</div> |
|||
@if (requireApprovement) { |
|||
<div class="col-lg-2 col-md-6"> |
|||
<div class="mb-3"> |
|||
<label class="mb-1 form-label" for="commentApproveState"> |
|||
{{ 'CmsKit::CommentFilter:ApproveState' | abpLocalization }} |
|||
</label> |
|||
<select |
|||
id="commentApproveState" |
|||
formControlName="commentApproveState" |
|||
class="form-control" |
|||
> |
|||
@for (option of commentApproveStateOptions; track option.value) { |
|||
<option [value]="option.value"> |
|||
{{ 'CmsKit::CommentFilter:' + option.value | abpLocalization }} |
|||
</option> |
|||
} |
|||
</select> |
|||
</div> |
|||
</div> |
|||
} |
|||
<div class="col-lg-2 col-md-6"> |
|||
<abp-button class="w-100 mb-3" buttonType="Primary" type="submit"> |
|||
<i class="fa fa-search" aria-hidden="true"></i> |
|||
</abp-button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
</abp-page> |
|||
@ -0,0 +1,101 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; |
|||
import { NgbDateAdapter, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; |
|||
import { ExtensibleModule, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { PageModule } from '@abp/ng.components/page'; |
|||
import { ButtonComponent, DateTimeAdapter, FormInputComponent } from '@abp/ng.theme.shared'; |
|||
import { |
|||
CommentAdminService, |
|||
CommentGetListInput, |
|||
CommentWithAuthorDto, |
|||
CommentApproveState, |
|||
commentApproveStateOptions, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { CommentEntityService } from '../../../services'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-comment-list', |
|||
templateUrl: './comment-list.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.CommentList, |
|||
}, |
|||
], |
|||
viewProviders: [ |
|||
{ |
|||
provide: NgbDateAdapter, |
|||
useClass: DateTimeAdapter, |
|||
}, |
|||
], |
|||
imports: [ |
|||
ExtensibleModule, |
|||
PageModule, |
|||
ReactiveFormsModule, |
|||
NgbDatepickerModule, |
|||
CommonModule, |
|||
LocalizationPipe, |
|||
FormInputComponent, |
|||
ButtonComponent, |
|||
], |
|||
}) |
|||
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); |
|||
|
|||
filterForm!: FormGroup; |
|||
commentApproveStateOptions = commentApproveStateOptions; |
|||
requireApprovement: boolean; |
|||
|
|||
ngOnInit() { |
|||
this.createFilterForm(); |
|||
this.hookToQuery(); |
|||
this.requireApprovement = this.commentEntityService.requireApprovement; |
|||
} |
|||
|
|||
private createFilterForm() { |
|||
this.filterForm = this.fb.group({ |
|||
creationStartDate: [null], |
|||
creationEndDate: [null], |
|||
author: [''], |
|||
entityType: [''], |
|||
commentApproveState: [CommentApproveState.All], |
|||
}); |
|||
} |
|||
|
|||
onFilter() { |
|||
const formValue = this.filterForm.value; |
|||
const filters: Partial<CommentGetListInput> = { |
|||
author: formValue.author || undefined, |
|||
entityType: formValue.entityType || undefined, |
|||
commentApproveState: formValue.commentApproveState, |
|||
creationStartDate: formValue.creationStartDate || undefined, |
|||
creationEndDate: formValue.creationEndDate || undefined, |
|||
}; |
|||
|
|||
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)); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export const CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT = 'CmsKit.Comments.RequireApprovement'; |
|||
@ -0,0 +1,3 @@ |
|||
export * from './comment-list/comment-list.component'; |
|||
export * from './comment-details/comment-details.component'; |
|||
export * from './constants'; |
|||
@ -0,0 +1,39 @@ |
|||
<abp-page [title]="'CmsKit::GlobalResources' | abpLocalization"> |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<div class="card"> |
|||
<div class="card-body"> |
|||
<ul |
|||
ngbNav |
|||
#nav="ngbNav" |
|||
class="nav-tabs" |
|||
[(activeId)]="activeTab" |
|||
(activeIdChange)="onTabChange($event)" |
|||
> |
|||
<li ngbNavItem="script"> |
|||
<a ngbNavLink>{{ 'CmsKit::Script' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<abp-codemirror-editor formControlName="script" /> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
<li ngbNavItem="style"> |
|||
<a ngbNavLink>{{ 'CmsKit::Style' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<abp-codemirror-editor formControlName="style" /> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
</ul> |
|||
<div class="mt-2 fade-in-top" [ngbNavOutlet]="nav"></div> |
|||
</div> |
|||
<div class="card-footer"> |
|||
<!-- *abpPermission="'CmsKit.GlobalResources.Update'" --> |
|||
<abp-button type="submit" [disabled]="form?.invalid" (click)="save()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</abp-page> |
|||
@ -0,0 +1,95 @@ |
|||
import { Component, OnInit, inject, DestroyRef } from '@angular/core'; |
|||
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { ButtonComponent, ToasterService } from '@abp/ng.theme.shared'; |
|||
import { CodeMirrorEditorComponent } from '@abp/ng.cms-kit'; |
|||
import { |
|||
GlobalResourceAdminService, |
|||
GlobalResourcesDto, |
|||
GlobalResourcesUpdateDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-global-resources', |
|||
imports: [ |
|||
CommonModule, |
|||
ReactiveFormsModule, |
|||
NgxValidateCoreModule, |
|||
NgbNavModule, |
|||
CodeMirrorEditorComponent, |
|||
LocalizationPipe, |
|||
PageComponent, |
|||
ButtonComponent, |
|||
], |
|||
templateUrl: './global-resources.component.html', |
|||
}) |
|||
export class GlobalResourcesComponent implements OnInit { |
|||
private globalResourceService = inject(GlobalResourceAdminService); |
|||
private toasterService = inject(ToasterService); |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
form: FormGroup; |
|||
activeTab: string = 'script'; |
|||
|
|||
ngOnInit() { |
|||
this.buildForm(); |
|||
this.loadGlobalResources(); |
|||
} |
|||
|
|||
private buildForm() { |
|||
this.form = new FormGroup({ |
|||
script: new FormControl(''), |
|||
style: new FormControl(''), |
|||
}); |
|||
} |
|||
|
|||
private loadGlobalResources() { |
|||
this.globalResourceService |
|||
.get() |
|||
.pipe(takeUntilDestroyed(this.destroyRef)) |
|||
.subscribe({ |
|||
next: (result: GlobalResourcesDto) => { |
|||
this.form.patchValue({ |
|||
script: result.scriptContent || '', |
|||
style: result.styleContent || '', |
|||
}); |
|||
}, |
|||
error: () => { |
|||
this.toasterService.error('AbpUi::ErrorMessage'); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
onTabChange(activeId: string) { |
|||
this.activeTab = activeId; |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid) { |
|||
return; |
|||
} |
|||
|
|||
const formValue = this.form.value; |
|||
const input: GlobalResourcesUpdateDto = { |
|||
script: formValue.script || '', |
|||
style: formValue.style || '', |
|||
}; |
|||
|
|||
this.globalResourceService |
|||
.setGlobalResources(input) |
|||
.pipe(takeUntilDestroyed(this.destroyRef)) |
|||
.subscribe({ |
|||
next: () => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}, |
|||
error: () => { |
|||
this.toasterService.error('AbpUi::ErrorMessage'); |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './global-resources.component'; |
|||
@ -0,0 +1,8 @@ |
|||
export * from './comments'; |
|||
export * from './tags'; |
|||
export * from './pages'; |
|||
export * from './blogs'; |
|||
export * from './blog-posts'; |
|||
export * from './menus'; |
|||
export * from './cms-settings'; |
|||
export * from './global-resources'; |
|||
@ -0,0 +1,2 @@ |
|||
export * from './menu-item-list/menu-item-list.component'; |
|||
export * from './menu-item-modal/menu-item-modal.component'; |
|||
@ -0,0 +1,56 @@ |
|||
<abp-page [title]="'CmsKit::MenuItems' | abpLocalization" [toolbar]="nodes"> |
|||
<div class="card"> |
|||
<div class="card-body"> |
|||
@if (nodes.length > 0) { |
|||
<abp-tree |
|||
[nodes]="nodes" |
|||
[draggable]="draggable" |
|||
[expandedKeys]="expandedKeys" |
|||
[selectedNode]="selectedNode" |
|||
(selectedNodeChange)="onSelectedNodeChange($event)" |
|||
[beforeDrop]="beforeDrop" |
|||
(dropOver)="onDrop($event)" |
|||
> |
|||
<ng-template #menu let-node> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="edit(node.key)" |
|||
*abpPermission="'CmsKit.Menus.Update'" |
|||
> |
|||
<i class="fa fa-pencil me-2"></i> |
|||
{{ 'AbpUi::Edit' | abpLocalization }} |
|||
</button> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="addSubMenuItem(node.key)" |
|||
*abpPermission="'CmsKit.Menus.Create'" |
|||
> |
|||
<i class="fa fa-plus me-2"></i> |
|||
{{ 'CmsKit::AddSubMenuItem' | abpLocalization }} |
|||
</button> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="delete(node.key, node.title)" |
|||
*abpPermission="'CmsKit.Menus.Delete'" |
|||
> |
|||
<i class="fa fa-remove me-2"></i> |
|||
{{ 'AbpUi::Delete' | abpLocalization }} |
|||
</button> |
|||
</ng-template> |
|||
</abp-tree> |
|||
} @else { |
|||
<div class="text-muted text-center"> |
|||
{{ 'CmsKit::NoMenuItems' | abpLocalization }} |
|||
</div> |
|||
} |
|||
</div> |
|||
</div> |
|||
</abp-page> |
|||
|
|||
@if (isModalVisible) { |
|||
<abp-menu-item-modal |
|||
[selected]="selectedMenuItem || undefined" |
|||
[parentId]="parentId" |
|||
(visibleChange)="onVisibleModalChange($event)" |
|||
/> |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { of } from 'rxjs'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { ListService, LocalizationPipe, PermissionDirective } from '@abp/ng.core'; |
|||
import { TreeComponent } from '@abp/ng.components/tree'; |
|||
import { EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; |
|||
import { |
|||
MenuItemAdminService, |
|||
MenuItemDto, |
|||
MenuItemWithDetailsDto, |
|||
MenuItemMoveInput, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { |
|||
MenuItemModalComponent, |
|||
MenuItemModalVisibleChange, |
|||
} from '../menu-item-modal/menu-item-modal.component'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-menu-item-list', |
|||
templateUrl: './menu-item-list.component.html', |
|||
imports: [ |
|||
PageComponent, |
|||
TreeComponent, |
|||
LocalizationPipe, |
|||
CommonModule, |
|||
MenuItemModalComponent, |
|||
PermissionDirective, |
|||
], |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.Menus, |
|||
}, |
|||
], |
|||
}) |
|||
export class MenuItemListComponent implements OnInit { |
|||
private menuItemService = inject(MenuItemAdminService); |
|||
private confirmationService = inject(ConfirmationService); |
|||
|
|||
nodes: any[] = []; |
|||
selectedNode: MenuItemDto | null = null; |
|||
expandedKeys: string[] = []; |
|||
draggable = true; |
|||
isModalVisible = false; |
|||
selectedMenuItem: MenuItemDto | MenuItemWithDetailsDto | null = null; |
|||
parentId: string | null = null; |
|||
|
|||
ngOnInit() { |
|||
this.loadMenuItems(); |
|||
} |
|||
|
|||
private loadMenuItems() { |
|||
this.menuItemService.getList().subscribe(result => { |
|||
if (result.items && result.items.length > 0) { |
|||
this.nodes = this.buildTreeNodes(result.items); |
|||
// Expand all nodes by default
|
|||
this.expandedKeys = this.nodes.map(n => n.key); |
|||
} else { |
|||
this.nodes = []; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private buildTreeNodes(items: MenuItemDto[]): any[] { |
|||
const nodeMap = new Map<string, any>(); |
|||
const rootNodes: any[] = []; |
|||
|
|||
// First pass: create all nodes
|
|||
items.forEach(item => { |
|||
const node: any = { |
|||
key: item.id, |
|||
title: item.displayName || '', |
|||
entity: item, |
|||
children: [], |
|||
isLeaf: false, |
|||
}; |
|||
nodeMap.set(item.id!, node); |
|||
}); |
|||
|
|||
// Second pass: build tree structure
|
|||
items.forEach(item => { |
|||
const node = nodeMap.get(item.id!); |
|||
if (item.parentId) { |
|||
const parent = nodeMap.get(item.parentId); |
|||
if (parent) { |
|||
parent.children.push(node); |
|||
parent.isLeaf = false; |
|||
} else { |
|||
rootNodes.push(node); |
|||
} |
|||
} else { |
|||
rootNodes.push(node); |
|||
} |
|||
}); |
|||
|
|||
// Sort by order
|
|||
const sortByOrder = (nodes: any[]) => { |
|||
nodes.sort((a, b) => (a.entity.order || 0) - (b.entity.order || 0)); |
|||
nodes.forEach(node => { |
|||
if (node.children && node.children.length > 0) { |
|||
sortByOrder(node.children); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
sortByOrder(rootNodes); |
|||
return rootNodes; |
|||
} |
|||
|
|||
onSelectedNodeChange(node: any) { |
|||
this.selectedNode = node?.entity || null; |
|||
} |
|||
|
|||
onDrop(event: any) { |
|||
const node = event.dragNode?.origin?.entity; |
|||
if (!node) { |
|||
return; |
|||
} |
|||
|
|||
const newParentId = event.dragNode?.parent?.key === '0' ? null : event.dragNode?.parent?.key; |
|||
const position = event.dragNode?.pos || 0; |
|||
|
|||
const parentNodeName = |
|||
!newParentId || newParentId === '0' |
|||
? 'Root' |
|||
: event.dragNode?.parent?.origin?.entity?.displayName || 'Root'; |
|||
|
|||
this.confirmationService |
|||
.warn('CmsKit::MenuItemMoveConfirmMessage', 'AbpUi::AreYouSure', { |
|||
messageLocalizationParams: [node.displayName || '', parentNodeName], |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
const input: MenuItemMoveInput = { |
|||
newParentId: newParentId === '0' ? null : newParentId, |
|||
position: position, |
|||
}; |
|||
|
|||
this.menuItemService.moveMenuItem(node.id!, input).subscribe({ |
|||
next: () => { |
|||
this.loadMenuItems(); |
|||
}, |
|||
error: () => { |
|||
// Reload to rollback
|
|||
this.loadMenuItems(); |
|||
}, |
|||
}); |
|||
} else { |
|||
// Reload to rollback
|
|||
this.loadMenuItems(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
beforeDrop = (event: any) => { |
|||
return of(true); |
|||
}; |
|||
|
|||
add() { |
|||
this.selectedMenuItem = null; |
|||
this.parentId = null; |
|||
this.isModalVisible = true; |
|||
} |
|||
|
|||
addSubMenuItem(parentId?: string) { |
|||
this.selectedMenuItem = null; |
|||
this.parentId = parentId || null; |
|||
this.isModalVisible = true; |
|||
} |
|||
|
|||
edit(id: string) { |
|||
this.menuItemService.get(id).subscribe(menuItem => { |
|||
this.selectedMenuItem = menuItem; |
|||
this.parentId = null; |
|||
this.isModalVisible = true; |
|||
}); |
|||
} |
|||
|
|||
onVisibleModalChange(visibilityChange: MenuItemModalVisibleChange) { |
|||
if (visibilityChange.visible) { |
|||
return; |
|||
} |
|||
if (visibilityChange.refresh) { |
|||
this.loadMenuItems(); |
|||
} |
|||
this.selectedMenuItem = null; |
|||
this.parentId = null; |
|||
this.isModalVisible = false; |
|||
} |
|||
|
|||
delete(id: string, displayName?: string) { |
|||
this.confirmationService |
|||
.warn('CmsKit::MenuItemDeletionConfirmationMessage', 'AbpUi::AreYouSure', { |
|||
messageLocalizationParams: [displayName || ''], |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.menuItemService.delete(id).subscribe({ |
|||
next: () => { |
|||
this.loadMenuItems(); |
|||
}, |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
<abp-modal [visible]="visible()" (visibleChange)="onVisibleChange($event)"> |
|||
<ng-template #abpHeader> |
|||
<h3> |
|||
@if (selected()?.id) { |
|||
{{ 'AbpUi::Edit' | abpLocalization }} |
|||
} @else { |
|||
{{ 'AbpUi::New' | abpLocalization }} |
|||
} |
|||
</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<ul |
|||
ngbNav |
|||
#nav="ngbNav" |
|||
class="nav-tabs" |
|||
[(activeId)]="activeTab" |
|||
(activeIdChange)="onTabChange($event)" |
|||
> |
|||
<li ngbNavItem="url"> |
|||
<a ngbNavLink>{{ 'CmsKit::Url' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<label class="form-label">{{ 'CmsKit::Url' | abpLocalization }}</label> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
formControlName="url" |
|||
[disabled]="isPageSelected" |
|||
(input)="onUrlInput()" |
|||
/> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
<li ngbNavItem="page"> |
|||
<a ngbNavLink>{{ 'CmsKit::Page' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<label class="form-label">{{ 'CmsKit::Page' | abpLocalization }}</label> |
|||
<div |
|||
class="position-relative" |
|||
ngbDropdown |
|||
#pageDropdown="ngbDropdown" |
|||
display="static" |
|||
(openChange)="onDropdownOpen()" |
|||
> |
|||
<button |
|||
class="form-select form-control text-start d-flex align-items-center justify-content-between" |
|||
type="button" |
|||
id="pageSelectDropdown" |
|||
ngbDropdownToggle |
|||
[class.text-muted]="!selectedPage" |
|||
> |
|||
<span>{{ selectedPage?.title || ('CmsKit::Page' | abpLocalization) }}</span> |
|||
</button> |
|||
<div |
|||
class="dropdown-menu w-100" |
|||
ngbDropdownMenu |
|||
aria-labelledby="pageSelectDropdown" |
|||
style="max-height: 300px; overflow-y: auto" |
|||
> |
|||
<div class="p-2 border-bottom"> |
|||
<input |
|||
type="text" |
|||
class="form-control form-control-sm" |
|||
[(ngModel)]="pageSearchText" |
|||
[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) { |
|||
<button |
|||
type="button" |
|||
class="dropdown-item" |
|||
(click)="selectPage(page); pageDropdown.close()" |
|||
> |
|||
{{ page.title }} |
|||
</button> |
|||
} |
|||
} @else if (pageSearchText && pageSearchText.length > 0) { |
|||
<div class="dropdown-item text-muted"> |
|||
{{ 'No results found' }} |
|||
</div> |
|||
} @else if (pages.length === 0) { |
|||
<div class="dropdown-item text-muted"> |
|||
{{ 'No pages found' }} |
|||
</div> |
|||
} |
|||
@if (selectedPage) { |
|||
<div class="dropdown-divider"></div> |
|||
<button |
|||
type="button" |
|||
class="dropdown-item text-danger" |
|||
(click)="clearPageSelection(); pageDropdown.close()" |
|||
> |
|||
{{ 'AbpUi::Clear' | abpLocalization }} |
|||
</button> |
|||
} |
|||
</div> |
|||
<input [formControlName]="'pageId'" type="hidden" /> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
</ul> |
|||
<div class="mt-2 fade-in-top" [ngbNavOutlet]="nav"></div> |
|||
<hr /> |
|||
<abp-extensible-form [selectedRecord]="selected() || {}" /> |
|||
</form> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button (click)="save()" [disabled]="form?.invalid"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,399 @@ |
|||
import { Component, OnInit, inject, Injector, input, output, DestroyRef } from '@angular/core'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormGroup, FormControl, FormsModule } from '@angular/forms'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { NgbNavModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { forkJoin, Subject } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ExtensibleFormComponent, |
|||
FormPropData, |
|||
generateFormFromProps, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { dasharize } from '@abp/ng.cms-kit'; |
|||
import { |
|||
MenuItemAdminService, |
|||
MenuItemDto, |
|||
MenuItemWithDetailsDto, |
|||
MenuItemCreateInput, |
|||
MenuItemUpdateInput, |
|||
PageLookupDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
export interface MenuItemModalVisibleChange { |
|||
visible: boolean; |
|||
refresh: boolean; |
|||
} |
|||
|
|||
// Constants
|
|||
const PAGE_LOOKUP_MAX_RESULT = 1000; |
|||
const PAGE_SEARCH_MAX_RESULT = 100; |
|||
const PAGE_SEARCH_DEBOUNCE_MS = 300; |
|||
const TABS = { |
|||
URL: 'url', |
|||
PAGE: 'page', |
|||
} as const; |
|||
|
|||
@Component({ |
|||
selector: 'abp-menu-item-modal', |
|||
templateUrl: './menu-item-modal.component.html', |
|||
imports: [ |
|||
ExtensibleFormComponent, |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
NgbNavModule, |
|||
NgbDropdownModule, |
|||
FormsModule, |
|||
], |
|||
styles: [ |
|||
` |
|||
.dropdown-toggle::after { |
|||
display: none !important; |
|||
} |
|||
`,
|
|||
], |
|||
}) |
|||
export class MenuItemModalComponent implements OnInit { |
|||
// Injected services
|
|||
private readonly menuItemService = inject(MenuItemAdminService); |
|||
private readonly injector = inject(Injector); |
|||
private readonly toasterService = inject(ToasterService); |
|||
private readonly destroyRef = inject(DestroyRef); |
|||
|
|||
// Inputs/Outputs
|
|||
readonly selected = input<MenuItemWithDetailsDto | MenuItemDto>(); |
|||
readonly parentId = input<string | null>(); |
|||
readonly visible = input<boolean>(true); |
|||
readonly visibleChange = output<MenuItemModalVisibleChange>(); |
|||
|
|||
// Form state
|
|||
form: FormGroup; |
|||
activeTab: string = TABS.URL; |
|||
|
|||
// Page selection state
|
|||
pages: PageLookupDto[] = []; |
|||
selectedPage: PageLookupDto | null = null; |
|||
pageSearchText: string = ''; |
|||
filteredPages: PageLookupDto[] = []; |
|||
|
|||
// Search subject for debouncing
|
|||
private readonly pageSearchSubject = new Subject<string>(); |
|||
|
|||
get isPageSelected(): boolean { |
|||
return !!this.form?.get('pageId')?.value; |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.setupPageSearch(); |
|||
this.initializeComponent(); |
|||
} |
|||
|
|||
/** |
|||
* Sets up debounced page search functionality |
|||
*/ |
|||
private setupPageSearch(): void { |
|||
this.pageSearchSubject |
|||
.pipe( |
|||
debounceTime(PAGE_SEARCH_DEBOUNCE_MS), |
|||
distinctUntilChanged(), |
|||
switchMap(searchText => { |
|||
if (!searchText?.trim()) { |
|||
// Show all pages when search is cleared
|
|||
return this.menuItemService.getPageLookup({ maxResultCount: PAGE_LOOKUP_MAX_RESULT }); |
|||
} |
|||
return this.menuItemService.getPageLookup({ |
|||
filter: searchText.trim(), |
|||
maxResultCount: PAGE_SEARCH_MAX_RESULT, |
|||
}); |
|||
}), |
|||
takeUntilDestroyed(this.destroyRef), |
|||
) |
|||
.subscribe({ |
|||
next: result => { |
|||
this.filteredPages = result.items || []; |
|||
}, |
|||
error: () => { |
|||
this.filteredPages = []; |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Initializes the component based on create or edit mode |
|||
*/ |
|||
private initializeComponent(): void { |
|||
const selectedItem = this.selected(); |
|||
|
|||
if (selectedItem?.id) { |
|||
this.loadMenuItemForEdit(selectedItem.id); |
|||
} else { |
|||
this.loadPagesForCreate(); |
|||
this.buildForm(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Loads menu item data and pages for edit mode |
|||
*/ |
|||
private loadMenuItemForEdit(menuItemId: string): void { |
|||
forkJoin({ |
|||
menuItem: this.menuItemService.get(menuItemId), |
|||
pages: this.menuItemService.getPageLookup({ maxResultCount: PAGE_LOOKUP_MAX_RESULT }), |
|||
}) |
|||
.pipe(takeUntilDestroyed(this.destroyRef)) |
|||
.subscribe({ |
|||
next: ({ menuItem, pages }) => { |
|||
this.pages = pages.items || []; |
|||
this.filteredPages = this.pages; |
|||
this.buildForm(menuItem); |
|||
this.initializePageSelection(menuItem); |
|||
}, |
|||
error: () => { |
|||
this.toasterService.error('AbpUi::ErrorMessage'); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Loads pages for create mode |
|||
*/ |
|||
private loadPagesForCreate(): void { |
|||
this.menuItemService |
|||
.getPageLookup({ maxResultCount: PAGE_LOOKUP_MAX_RESULT }) |
|||
.pipe(takeUntilDestroyed(this.destroyRef)) |
|||
.subscribe({ |
|||
next: result => { |
|||
this.pages = result.items || []; |
|||
this.filteredPages = this.pages; |
|||
}, |
|||
error: () => { |
|||
this.toasterService.error('AbpUi::ErrorMessage'); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Initializes page selection when editing a menu item with a page |
|||
*/ |
|||
private initializePageSelection(menuItem: MenuItemWithDetailsDto | MenuItemDto): void { |
|||
if (menuItem.pageId) { |
|||
this.activeTab = TABS.PAGE; |
|||
this.selectedPage = this.pages.find(p => p.id === menuItem.pageId) || null; |
|||
|
|||
const url = this.selectedPage |
|||
? this.generateUrlFromPage(this.selectedPage) |
|||
: menuItem.url || ''; |
|||
this.form.patchValue({ pageId: menuItem.pageId, url }, { emitEvent: false }); |
|||
this.pageSearchText = this.selectedPage?.title || ''; |
|||
} else if (menuItem.url) { |
|||
this.activeTab = TABS.URL; |
|||
this.form.patchValue({ url: menuItem.url, pageId: null }, { emitEvent: false }); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Generates a URL from a page's slug or title |
|||
*/ |
|||
private generateUrlFromPage(page: PageLookupDto): string { |
|||
if (!page) return ''; |
|||
|
|||
const source = page.slug || page.title; |
|||
if (!source) return ''; |
|||
|
|||
return '/' + dasharize(source); |
|||
} |
|||
|
|||
/** |
|||
* Handles page search input changes |
|||
*/ |
|||
onPageSearchChange(searchText: string): void { |
|||
this.pageSearchText = searchText; |
|||
if (!searchText?.trim()) { |
|||
this.filteredPages = this.pages; |
|||
return; |
|||
} |
|||
this.pageSearchSubject.next(searchText); |
|||
} |
|||
|
|||
/** |
|||
* Handles dropdown open event |
|||
*/ |
|||
onDropdownOpen(): void { |
|||
if (!this.pageSearchText?.trim()) { |
|||
this.filteredPages = this.pages; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Handles page selection from dropdown |
|||
*/ |
|||
selectPage(page: PageLookupDto): void { |
|||
if (!page) return; |
|||
|
|||
this.selectedPage = page; |
|||
const url = this.generateUrlFromPage(page); |
|||
|
|||
this.form.patchValue({ pageId: page.id, url }, { emitEvent: false }); |
|||
this.pageSearchText = page.title || ''; |
|||
} |
|||
|
|||
/** |
|||
* Clears the selected page |
|||
*/ |
|||
clearPageSelection(): void { |
|||
this.form.patchValue({ pageId: null }, { emitEvent: false }); |
|||
this.selectedPage = null; |
|||
this.pageSearchText = ''; |
|||
this.filteredPages = this.pages; |
|||
} |
|||
|
|||
/** |
|||
* Builds the reactive form for menu item creation/editing |
|||
*/ |
|||
private buildForm(menuItem?: MenuItemWithDetailsDto | MenuItemDto): void { |
|||
const data = new FormPropData(this.injector, menuItem || {}); |
|||
const baseForm = generateFormFromProps(data); |
|||
const parentId = this.parentId() || menuItem?.parentId || null; |
|||
|
|||
this.form = new FormGroup({ |
|||
...baseForm.controls, |
|||
url: new FormControl(menuItem?.url || ''), |
|||
pageId: new FormControl(menuItem?.pageId || null), |
|||
parentId: new FormControl(parentId), |
|||
}); |
|||
|
|||
this.loadAvailableOrder(parentId, menuItem?.id); |
|||
} |
|||
|
|||
/** |
|||
* 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
|
|||
|
|||
const order$ = parentId |
|||
? this.menuItemService.getAvailableMenuOrder(parentId) |
|||
: this.menuItemService.getAvailableMenuOrder(); |
|||
|
|||
order$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ |
|||
next: order => { |
|||
this.form.patchValue({ order }); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Handles modal visibility changes |
|||
*/ |
|||
onVisibleChange(visible: boolean, refresh = false): void { |
|||
this.visibleChange.emit({ visible, refresh }); |
|||
} |
|||
|
|||
/** |
|||
* Handles tab changes |
|||
*/ |
|||
onTabChange(activeId: string): void { |
|||
this.activeTab = activeId; |
|||
} |
|||
|
|||
/** |
|||
* Handles URL input changes - clears page selection if URL is manually entered |
|||
*/ |
|||
onUrlInput(): void { |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Saves the menu item (create or update) |
|||
*/ |
|||
save(): void { |
|||
if (!this.form.valid) { |
|||
return; |
|||
} |
|||
|
|||
const formValue = this.prepareFormValue(); |
|||
const selectedItem = this.selected(); |
|||
const isEditMode = !!selectedItem?.id; |
|||
|
|||
const observable$ = isEditMode |
|||
? this.updateMenuItem(selectedItem.id, formValue, selectedItem as MenuItemWithDetailsDto) |
|||
: this.createMenuItem(formValue); |
|||
|
|||
observable$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ |
|||
next: () => { |
|||
this.onVisibleChange(false, true); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}, |
|||
error: () => { |
|||
this.toasterService.error('AbpUi::ErrorMessage'); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Prepares form value ensuring mutual exclusivity between pageId and url |
|||
*/ |
|||
private prepareFormValue(): Partial<MenuItemCreateInput | MenuItemUpdateInput> { |
|||
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); |
|||
} |
|||
} 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, |
|||
pageId: formValue.pageId || undefined, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Creates a new menu item |
|||
*/ |
|||
private createMenuItem(formValue: Partial<MenuItemCreateInput>) { |
|||
const createInput: MenuItemCreateInput = formValue as MenuItemCreateInput; |
|||
return this.menuItemService.create(createInput); |
|||
} |
|||
|
|||
/** |
|||
* Updates an existing menu item |
|||
*/ |
|||
private updateMenuItem( |
|||
id: string, |
|||
formValue: Partial<MenuItemUpdateInput>, |
|||
selectedItem: MenuItemWithDetailsDto, |
|||
) { |
|||
const updateInput: MenuItemUpdateInput = { |
|||
...formValue, |
|||
concurrencyStamp: selectedItem.concurrencyStamp, |
|||
} as MenuItemUpdateInput; |
|||
return this.menuItemService.update(id, updateInput); |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export * from './page-list/page-list.component'; |
|||
export * from './page-form/page-form.component'; |
|||
@ -0,0 +1,57 @@ |
|||
<abp-page [title]="'CmsKit::Pages' | abpLocalization"> |
|||
<div class="card"> |
|||
<div class="card-body"> |
|||
@if (form && (!isEditMode || page)) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<!-- Basic fields outside tabs --> |
|||
<abp-extensible-form [selectedRecord]="page || {}" /> |
|||
|
|||
<!-- Tabs for Content, Script, and Style --> |
|||
<ul ngbNav #nav="ngbNav" class="nav-tabs mt-3"> |
|||
<li ngbNavItem> |
|||
<a ngbNavLink>{{ 'CmsKit::Content' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<abp-toastui-editor formControlName="content" /> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
|
|||
<li ngbNavItem> |
|||
<a ngbNavLink>{{ 'CmsKit::Script' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<abp-codemirror-editor formControlName="script" /> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
|
|||
<li ngbNavItem> |
|||
<a ngbNavLink>{{ 'CmsKit::Style' | abpLocalization }}</a> |
|||
<ng-template ngbNavContent> |
|||
<div class="mt-3"> |
|||
<abp-codemirror-editor formControlName="style" /> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
</ul> |
|||
<div class="mt-2 fade-in-top" [ngbNavOutlet]="nav"></div> |
|||
</form> |
|||
} @else { |
|||
<div class="text-center"> |
|||
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i> |
|||
</div> |
|||
} |
|||
</div> |
|||
<div class="card-footer"> |
|||
<div class="d-flex justify-content-start gap-2"> |
|||
<button class="btn btn-secondary" (click)="saveAsDraft()" [disabled]="form?.invalid"> |
|||
{{ 'CmsKit::SaveAsDraft' | abpLocalization }} |
|||
</button> |
|||
<abp-button (click)="publish()" [disabled]="form?.invalid"> |
|||
{{ 'CmsKit::Publish' | abpLocalization }} |
|||
</abp-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</abp-page> |
|||
@ -0,0 +1,134 @@ |
|||
import { Component, OnInit, inject, Injector, DestroyRef } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms'; |
|||
import { ActivatedRoute } from '@angular/router'; |
|||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ExtensibleFormComponent, |
|||
FormPropData, |
|||
generateFormFromProps, |
|||
EXTENSIONS_IDENTIFIER, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { ButtonComponent } from '@abp/ng.theme.shared'; |
|||
import { |
|||
ToastuiEditorComponent, |
|||
CodeMirrorEditorComponent, |
|||
prepareSlugFromControl, |
|||
} from '@abp/ng.cms-kit'; |
|||
import { PageAdminService, PageDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { PageFormService } from '../../../services'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-page-form', |
|||
templateUrl: './page-form.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.PageForm, |
|||
}, |
|||
], |
|||
imports: [ |
|||
ButtonComponent, |
|||
CodeMirrorEditorComponent, |
|||
ExtensibleFormComponent, |
|||
PageComponent, |
|||
ToastuiEditorComponent, |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
NgbNavModule, |
|||
], |
|||
}) |
|||
export class PageFormComponent implements OnInit { |
|||
private pageService = inject(PageAdminService); |
|||
private injector = inject(Injector); |
|||
private pageFormService = inject(PageFormService); |
|||
private route = inject(ActivatedRoute); |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
form: FormGroup; |
|||
page: PageDto | null = null; |
|||
pageId: string | null = null; |
|||
isEditMode = false; |
|||
|
|||
ngOnInit() { |
|||
const id = this.route.snapshot.params['id']; |
|||
if (id) { |
|||
this.isEditMode = true; |
|||
this.pageId = id; |
|||
this.loadPage(id); |
|||
} else { |
|||
this.isEditMode = false; |
|||
this.buildForm(); |
|||
} |
|||
} |
|||
|
|||
private loadPage(id: string) { |
|||
this.pageService.get(id).subscribe(page => { |
|||
this.page = page; |
|||
this.buildForm(); |
|||
}); |
|||
} |
|||
|
|||
private buildForm() { |
|||
const data = new FormPropData(this.injector, this.page || {}); |
|||
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 || ''), |
|||
}); |
|||
prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef); |
|||
} |
|||
|
|||
private executeSaveOperation(operation: 'save' | 'draft' | 'publish') { |
|||
if (this.isEditMode) { |
|||
if (!this.page || !this.pageId) { |
|||
return; |
|||
} |
|||
|
|||
switch (operation) { |
|||
case 'save': |
|||
this.pageFormService.update(this.pageId, this.form, this.page).subscribe(); |
|||
break; |
|||
case 'draft': |
|||
this.pageFormService.updateAsDraft(this.pageId, this.form, this.page).subscribe(); |
|||
break; |
|||
case 'publish': |
|||
this.pageFormService.updateAndPublish(this.pageId, this.form, this.page).subscribe(); |
|||
break; |
|||
} |
|||
return; |
|||
} |
|||
|
|||
switch (operation) { |
|||
case 'save': |
|||
this.pageFormService.create(this.form).subscribe(); |
|||
break; |
|||
case 'draft': |
|||
this.pageFormService.createAsDraft(this.form).subscribe(); |
|||
break; |
|||
case 'publish': |
|||
this.pageFormService.publish(this.form).subscribe(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
save() { |
|||
this.executeSaveOperation('save'); |
|||
} |
|||
|
|||
saveAsDraft() { |
|||
this.executeSaveOperation('draft'); |
|||
} |
|||
|
|||
publish() { |
|||
this.executeSaveOperation('publish'); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<abp-page [title]="'CmsKit::Pages' | abpLocalization" [toolbar]="data.items"> |
|||
<div class="card mb-4"> |
|||
<div class="card-body"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="input-group"> |
|||
<input |
|||
type="search" |
|||
class="form-control" |
|||
[placeholder]="'AbpUi::PagerSearch' | abpLocalization" |
|||
[(ngModel)]="filter" |
|||
(keyup.enter)="onSearch()" |
|||
/> |
|||
<button class="btn btn-primary" type="button" (click)="onSearch()"> |
|||
<i class="fa fa-search" aria-hidden="true"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
</abp-page> |
|||
@ -0,0 +1,85 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; |
|||
import { ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { Confirmation, ConfirmationService, ToasterService } from '@abp/ng.theme.shared'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { PageAdminService, GetPagesInputDto, PageDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-page-list', |
|||
templateUrl: './page-list.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.Pages, |
|||
}, |
|||
], |
|||
imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule], |
|||
}) |
|||
export class PageListComponent implements OnInit { |
|||
data: PagedResultDto<PageDto> = { items: [], totalCount: 0 }; |
|||
|
|||
public readonly list = inject(ListService<GetPagesInputDto>); |
|||
private pageService = inject(PageAdminService); |
|||
private confirmationService = inject(ConfirmationService); |
|||
private toasterService = inject(ToasterService); |
|||
|
|||
filter = ''; |
|||
|
|||
ngOnInit() { |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
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', { |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.pageService.delete(id).subscribe(() => { |
|||
this.list.get(); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
setAsHomePage(id: string, isHomePage: boolean) { |
|||
this.pageService.setAsHomePage(id).subscribe(() => { |
|||
this.list.get(); |
|||
if (isHomePage) { |
|||
this.toasterService.warn('CmsKit::RemovedSettingAsHomePage'); |
|||
} else { |
|||
this.toasterService.success('CmsKit::CompletedSettingAsHomePage'); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export * from './tag-list/tag-list.component'; |
|||
export * from './tag-modal/tag-modal.component'; |
|||
@ -0,0 +1,30 @@ |
|||
<abp-page [title]="'CmsKit::Tags' | abpLocalization" [toolbar]="data.items"> |
|||
<div class="card mb-4"> |
|||
<div class="card-body"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="input-group"> |
|||
<input |
|||
type="search" |
|||
class="form-control" |
|||
[placeholder]="'AbpUi::PagerSearch' | abpLocalization" |
|||
[(ngModel)]="filter" |
|||
(keyup.enter)="onSearch()" |
|||
/> |
|||
<button class="btn btn-primary" type="button" (click)="onSearch()"> |
|||
<i class="fa fa-search" aria-hidden="true"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="card"> |
|||
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list" /> |
|||
</div> |
|||
|
|||
@if (isModalVisible) { |
|||
<abp-tag-modal [selected]="selected" (visibleChange)="onVisibleModalChange($event)" /> |
|||
} |
|||
</abp-page> |
|||
@ -0,0 +1,113 @@ |
|||
import { Component, OnInit, inject } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; |
|||
import { ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { PageComponent } from '@abp/ng.components/page'; |
|||
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; |
|||
import { TagAdminService, TagGetListInput, TagDto, TagDefinitionDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { eCmsKitAdminComponents } from '../../../enums'; |
|||
import { TagModalComponent, TagModalVisibleChange } from '../tag-modal/tag-modal.component'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-tag-list', |
|||
templateUrl: './tag-list.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: eCmsKitAdminComponents.Tags, |
|||
}, |
|||
], |
|||
imports: [ |
|||
ExtensibleTableComponent, |
|||
PageComponent, |
|||
LocalizationPipe, |
|||
FormsModule, |
|||
CommonModule, |
|||
TagModalComponent, |
|||
], |
|||
}) |
|||
export class TagListComponent implements OnInit { |
|||
data: PagedResultDto<TagDto> = { items: [], totalCount: 0 }; |
|||
|
|||
public readonly list = inject(ListService<TagGetListInput>); |
|||
private tagService = inject(TagAdminService); |
|||
private confirmationService = inject(ConfirmationService); |
|||
|
|||
filter = ''; |
|||
isModalVisible = false; |
|||
selected?: TagDto; |
|||
tagDefinitions: TagDefinitionDto[] = []; |
|||
|
|||
ngOnInit() { |
|||
this.loadTagDefinitions(); |
|||
this.hookToQuery(); |
|||
} |
|||
|
|||
private loadTagDefinitions() { |
|||
this.tagService.getTagDefinitions().subscribe(definitions => { |
|||
this.tagDefinitions = definitions; |
|||
}); |
|||
} |
|||
|
|||
onSearch() { |
|||
this.list.filter = this.filter; |
|||
this.list.get(); |
|||
} |
|||
|
|||
add() { |
|||
this.selected = {} as TagDto; |
|||
this.isModalVisible = true; |
|||
} |
|||
|
|||
edit(id: string) { |
|||
this.tagService.get(id).subscribe(tag => { |
|||
this.selected = tag; |
|||
this.isModalVisible = 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; |
|||
} |
|||
if (visibilityChange.refresh) { |
|||
this.list.get(); |
|||
} |
|||
this.selected = null; |
|||
this.isModalVisible = false; |
|||
} |
|||
|
|||
delete(id: string, name: string) { |
|||
this.confirmationService |
|||
.warn('CmsKit::TagDeletionConfirmationMessage', 'AbpUi::AreYouSure', { |
|||
messageLocalizationParams: [name], |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.tagService.delete(id).subscribe(() => { |
|||
this.list.get(); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<abp-modal [visible]="true" (visibleChange)="onVisibleChange($event)"> |
|||
<ng-template #abpHeader> |
|||
<h3> |
|||
{{ (selected()?.id ? 'AbpUi::Edit' : 'AbpUi::New') | abpLocalization }} |
|||
</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (form) { |
|||
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
|||
<abp-extensible-form [selectedRecord]="selected()" /> |
|||
</form> |
|||
} @else { |
|||
<div class="text-center"> |
|||
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i> |
|||
</div> |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,84 @@ |
|||
import { Component, OnInit, inject, Injector, input, output } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ReactiveFormsModule, FormGroup } from '@angular/forms'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ExtensibleFormComponent, |
|||
FormPropData, |
|||
generateFormFromProps, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { TagAdminService, TagDto, TagCreateDto, TagUpdateDto } from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
export interface TagModalVisibleChange { |
|||
visible: boolean; |
|||
refresh: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-tag-modal', |
|||
templateUrl: './tag-modal.component.html', |
|||
imports: [ |
|||
ExtensibleFormComponent, |
|||
LocalizationPipe, |
|||
ReactiveFormsModule, |
|||
CommonModule, |
|||
NgxValidateCoreModule, |
|||
ModalComponent, |
|||
ModalCloseDirective, |
|||
ButtonComponent, |
|||
], |
|||
}) |
|||
export class TagModalComponent implements OnInit { |
|||
private tagService = inject(TagAdminService); |
|||
private injector = inject(Injector); |
|||
private toasterService = inject(ToasterService); |
|||
|
|||
selected = input<TagDto>(); |
|||
sectionId = input<string>(); |
|||
visibleChange = output<TagModalVisibleChange>(); |
|||
|
|||
form: FormGroup; |
|||
|
|||
ngOnInit() { |
|||
this.buildForm(); |
|||
} |
|||
|
|||
private buildForm() { |
|||
const data = new FormPropData(this.injector, this.selected()); |
|||
this.form = generateFormFromProps(data); |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean, refresh = false) { |
|||
this.visibleChange.emit({ visible, refresh }); |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid) { |
|||
return; |
|||
} |
|||
|
|||
let observable$ = this.tagService.create(this.form.value as TagCreateDto); |
|||
|
|||
const selectedTag = this.selected(); |
|||
const { id } = selectedTag || {}; |
|||
|
|||
if (id) { |
|||
observable$ = this.tagService.update(id, { |
|||
...selectedTag, |
|||
...this.form.value, |
|||
} as TagUpdateDto); |
|||
} |
|||
|
|||
observable$.subscribe(() => { |
|||
this.onVisibleChange(false, true); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
import { Validators } from '@angular/forms'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { CreateBlogPostDto, BlogAdminService } from '@abp/ng.cms-kit/proxy'; |
|||
import { FormProp, ePropType } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_BLOG_POST_CREATE_FORM_PROPS = FormProp.createMany<CreateBlogPostDto>([ |
|||
{ |
|||
type: ePropType.Enum, |
|||
name: 'blogId', |
|||
displayName: 'CmsKit::Blog', |
|||
id: 'blogId', |
|||
options: data => { |
|||
const blogService = data.getInjected(BlogAdminService); |
|||
return blogService.getList({ maxResultCount: 1000 }).pipe( |
|||
map(result => |
|||
result.items.map(blog => ({ |
|||
key: blog.name || '', |
|||
value: blog.id || '', |
|||
})), |
|||
), |
|||
); |
|||
}, |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'title', |
|||
displayName: 'CmsKit::Title', |
|||
id: 'title', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
id: 'slug', |
|||
validators: () => [Validators.required], |
|||
tooltip: { |
|||
text: 'CmsKit::BlogPostSlugInformation', |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'shortDescription', |
|||
displayName: 'CmsKit::ShortDescription', |
|||
id: 'shortDescription', |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_BLOG_POST_EDIT_FORM_PROPS: FormProp<any>[] = |
|||
DEFAULT_BLOG_POST_CREATE_FORM_PROPS; |
|||
@ -0,0 +1,23 @@ |
|||
import { Router } from '@angular/router'; |
|||
import { EntityAction } from '@abp/ng.components/extensible'; |
|||
import { BlogPostListDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { BlogPostListComponent } from '../../components/blog-posts/blog-post-list/blog-post-list.component'; |
|||
|
|||
export const DEFAULT_BLOG_POST_ENTITY_ACTIONS = EntityAction.createMany<BlogPostListDto>([ |
|||
{ |
|||
text: 'AbpUi::Edit', |
|||
action: data => { |
|||
const router = data.getInjected(Router); |
|||
router.navigate(['/cms/blog-posts/update', data.record.id]); |
|||
}, |
|||
permission: 'CmsKit.BlogPosts.Update', |
|||
}, |
|||
{ |
|||
text: 'AbpUi::Delete', |
|||
action: data => { |
|||
const component = data.getInjected(BlogPostListComponent); |
|||
component.delete(data.record.id!, data.record.title!); |
|||
}, |
|||
permission: 'CmsKit.BlogPosts.Delete', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,65 @@ |
|||
import { of } from 'rxjs'; |
|||
import { BlogPostListDto, BlogPostStatus } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
|
|||
export const DEFAULT_BLOG_POST_ENTITY_PROPS = EntityProp.createMany<BlogPostListDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'blogName', |
|||
displayName: 'CmsKit::Blog', |
|||
sortable: true, |
|||
columnWidth: 150, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'title', |
|||
displayName: 'CmsKit::Title', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'status', |
|||
displayName: 'CmsKit::Status', |
|||
sortable: true, |
|||
columnWidth: 120, |
|||
valueResolver: data => { |
|||
const localization = data.getInjected(LocalizationService); |
|||
let result = ''; |
|||
switch (data.record.status) { |
|||
case BlogPostStatus.Draft: |
|||
result = localization.instant('CmsKit::CmsKit.BlogPost.Status.0'); |
|||
break; |
|||
case BlogPostStatus.Published: |
|||
result = localization.instant('CmsKit::CmsKit.BlogPost.Status.1'); |
|||
break; |
|||
case BlogPostStatus.WaitingForReview: |
|||
result = localization.instant('CmsKit::CmsKit.BlogPost.Status.2'); |
|||
break; |
|||
} |
|||
return of(result); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.Date, |
|||
name: 'creationTime', |
|||
displayName: 'AbpIdentity::CreationTime', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.Date, |
|||
name: 'lastModificationTime', |
|||
displayName: 'AbpIdentity::LastModificationTime', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
]); |
|||
@ -0,0 +1,15 @@ |
|||
import { Router } from '@angular/router'; |
|||
import { BlogPostListDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ToolbarAction } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_BLOG_POST_TOOLBAR_ACTIONS = ToolbarAction.createMany<BlogPostListDto[]>([ |
|||
{ |
|||
text: 'CmsKit::NewBlogPost', |
|||
action: data => { |
|||
const router = data.getInjected(Router); |
|||
router.navigate(['/cms/blog-posts/create']); |
|||
}, |
|||
permission: 'CmsKit.BlogPosts.Create', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,4 @@ |
|||
export * from './default-blog-post-entity-actions'; |
|||
export * from './default-blog-post-entity-props'; |
|||
export * from './default-blog-post-toolbar-actions'; |
|||
export * from './default-blog-post-create-form-props'; |
|||
@ -0,0 +1,26 @@ |
|||
import { CreateBlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { FormProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { Validators } from '@angular/forms'; |
|||
|
|||
export const DEFAULT_BLOG_CREATE_FORM_PROPS = FormProp.createMany<CreateBlogDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'name', |
|||
displayName: 'CmsKit::Name', |
|||
id: 'name', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
id: 'slug', |
|||
validators: () => [Validators.required], |
|||
tooltip: { |
|||
text: 'CmsKit::BlogSlugInformation', |
|||
// params: ['blogs'],
|
|||
}, |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_BLOG_EDIT_FORM_PROPS = DEFAULT_BLOG_CREATE_FORM_PROPS; |
|||
@ -0,0 +1,30 @@ |
|||
import { BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityAction } from '@abp/ng.components/extensible'; |
|||
import { BlogListComponent } from '../../components/blogs/blog-list/blog-list.component'; |
|||
|
|||
export const DEFAULT_BLOG_ENTITY_ACTIONS = EntityAction.createMany<BlogDto>([ |
|||
{ |
|||
text: 'CmsKit::Features', |
|||
action: data => { |
|||
const component = data.getInjected(BlogListComponent); |
|||
component.openFeatures(data.record.id!); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Features', |
|||
}, |
|||
{ |
|||
text: 'AbpUi::Edit', |
|||
action: data => { |
|||
const component = data.getInjected(BlogListComponent); |
|||
component.edit(data.record.id!); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Update', |
|||
}, |
|||
{ |
|||
text: 'AbpUi::Delete', |
|||
action: data => { |
|||
const component = data.getInjected(BlogListComponent); |
|||
component.delete(data.record.id!, data.record.name!); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Delete', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,19 @@ |
|||
import { BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityProp, ePropType } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_BLOG_ENTITY_PROPS = EntityProp.createMany<BlogDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'name', |
|||
displayName: 'CmsKit::Name', |
|||
sortable: true, |
|||
columnWidth: 250, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
sortable: true, |
|||
columnWidth: 250, |
|||
}, |
|||
]); |
|||
@ -0,0 +1,15 @@ |
|||
import { BlogDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ToolbarAction } from '@abp/ng.components/extensible'; |
|||
import { BlogListComponent } from '../../components/blogs/blog-list/blog-list.component'; |
|||
|
|||
export const DEFAULT_BLOG_TOOLBAR_ACTIONS = ToolbarAction.createMany<BlogDto[]>([ |
|||
{ |
|||
text: 'CmsKit::NewBlog', |
|||
action: data => { |
|||
const component = data.getInjected(BlogListComponent); |
|||
component.add(); |
|||
}, |
|||
permission: 'CmsKit.Blogs.Create', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,4 @@ |
|||
export * from './default-blog-entity-actions'; |
|||
export * from './default-blog-entity-props'; |
|||
export * from './default-blog-toolbar-actions'; |
|||
export * from './default-blog-create-form-props'; |
|||
@ -0,0 +1,51 @@ |
|||
import { Router } from '@angular/router'; |
|||
import { CommentGetListInput, CommentWithAuthorDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { EntityAction } from '@abp/ng.components/extensible'; |
|||
import { CommentEntityService } from '../../services'; |
|||
|
|||
export const DEFAULT_COMMENT_ENTITY_ACTIONS = EntityAction.createMany<CommentWithAuthorDto>([ |
|||
{ |
|||
text: 'CmsKit::Details', |
|||
action: data => { |
|||
const router = data.getInjected(Router); |
|||
router.navigate(['/cms/comments', data.record.id]); |
|||
}, |
|||
}, |
|||
{ |
|||
text: 'CmsKit::Delete', |
|||
action: data => { |
|||
const commentEntityService = data.getInjected(CommentEntityService); |
|||
const list = data.getInjected(ListService<CommentGetListInput>); |
|||
commentEntityService.delete(data.record.id!, list); |
|||
}, |
|||
permission: 'CmsKit.Comments.Delete', |
|||
}, |
|||
{ |
|||
text: 'CmsKit::Approve', |
|||
action: data => { |
|||
const commentEntityService = data.getInjected(CommentEntityService); |
|||
const list = data.getInjected(ListService<CommentGetListInput>); |
|||
commentEntityService.updateApprovalStatus(data.record.id!, true, list); |
|||
}, |
|||
visible: data => { |
|||
const commentEntityService = data.getInjected(CommentEntityService); |
|||
return commentEntityService.requireApprovement && data.record.isApproved === false; |
|||
}, |
|||
}, |
|||
{ |
|||
text: 'CmsKit::Disapproved', |
|||
action: data => { |
|||
const commentEntityService = data.getInjected(CommentEntityService); |
|||
const list = data.getInjected(ListService<CommentGetListInput>); |
|||
commentEntityService.updateApprovalStatus(data.record.id!, false, list); |
|||
}, |
|||
visible: data => { |
|||
const commentEntityService = data.getInjected(CommentEntityService); |
|||
return ( |
|||
commentEntityService.requireApprovement && |
|||
(data.record.isApproved || data.record.isApproved === null) |
|||
); |
|||
}, |
|||
}, |
|||
]); |
|||
@ -0,0 +1,79 @@ |
|||
import { of } from 'rxjs'; |
|||
import { EntityProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { CommentWithAuthorDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { CommentEntityService } from '../../services'; |
|||
|
|||
export const DEFAULT_COMMENT_ENTITY_PROPS = EntityProp.createMany<CommentWithAuthorDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'userName', |
|||
displayName: 'CmsKit::Username', |
|||
sortable: false, |
|||
columnWidth: 150, |
|||
valueResolver: data => { |
|||
const userName = data.record.author.userName; |
|||
return of(userName); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'entityType', |
|||
displayName: 'CmsKit::EntityType', |
|||
sortable: false, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'url', |
|||
displayName: 'CmsKit::URL', |
|||
sortable: false, |
|||
valueResolver: data => { |
|||
const url = data.record.url; |
|||
if (url) { |
|||
return of( |
|||
`<a href="${url}#comment-${data.record.id}" target="_blank"><i class="fa fa-location-arrow"></i></a>`, |
|||
); |
|||
} |
|||
return of(''); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'text', |
|||
displayName: 'CmsKit::Text', |
|||
sortable: false, |
|||
valueResolver: data => { |
|||
const text = data.record.text || ''; |
|||
const maxChars = 64; |
|||
if (text.length > maxChars) { |
|||
return of(text.substring(0, maxChars) + '...'); |
|||
} |
|||
return of(text); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'isApproved', |
|||
displayName: 'CmsKit::ApproveState', |
|||
sortable: false, |
|||
columnWidth: 100, |
|||
columnVisible: getInjected => { |
|||
const commentEntityService = getInjected(CommentEntityService); |
|||
return commentEntityService.requireApprovement; |
|||
}, |
|||
valueResolver: data => { |
|||
const isApproved = data.record.isApproved; |
|||
if (isApproved || isApproved === null) { |
|||
return of('<div class="text-success"><i class="fa fa-check" aria-hidden="true"></i></div>'); |
|||
} |
|||
return of('<div class="text-danger"><i class="fa fa-times" aria-hidden="true"></i></div>'); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.Date, |
|||
name: 'creationTime', |
|||
displayName: 'CmsKit::CreationTime', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
]); |
|||
@ -0,0 +1,2 @@ |
|||
export * from './default-comment-entity-actions'; |
|||
export * from './default-comment-entity-props'; |
|||
@ -0,0 +1,6 @@ |
|||
export * from './comments'; |
|||
export * from './tags'; |
|||
export * from './pages'; |
|||
export * from './blogs'; |
|||
export * from './blog-posts'; |
|||
export * from './menus'; |
|||
@ -0,0 +1,72 @@ |
|||
import { Validators } from '@angular/forms'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { |
|||
MenuItemCreateInput, |
|||
MenuItemAdminService, |
|||
PermissionLookupDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
import { FormProp, ePropType } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_MENU_ITEM_CREATE_FORM_PROPS = FormProp.createMany<MenuItemCreateInput>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'displayName', |
|||
displayName: 'CmsKit::DisplayName', |
|||
id: 'displayName', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.Boolean, |
|||
name: 'isActive', |
|||
displayName: 'CmsKit::IsActive', |
|||
id: 'isActive', |
|||
defaultValue: true, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'icon', |
|||
displayName: 'CmsKit::Icon', |
|||
id: 'icon', |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'target', |
|||
displayName: 'CmsKit::Target', |
|||
id: 'target', |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'elementId', |
|||
displayName: 'CmsKit::ElementId', |
|||
id: 'elementId', |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'cssClass', |
|||
displayName: 'CmsKit::CssClass', |
|||
id: 'cssClass', |
|||
}, |
|||
{ |
|||
type: ePropType.Enum, |
|||
name: 'requiredPermissionName', |
|||
displayName: 'CmsKit::RequiredPermissionName', |
|||
id: 'requiredPermissionName', |
|||
options: data => { |
|||
const menuItemService = data.getInjected(MenuItemAdminService); |
|||
return menuItemService |
|||
.getPermissionLookup({ |
|||
filter: '', |
|||
}) |
|||
.pipe( |
|||
map((result: { items: PermissionLookupDto[] }) => |
|||
result.items.map(permission => ({ |
|||
key: permission.displayName || permission.name || '', |
|||
value: permission.name || '', |
|||
})), |
|||
), |
|||
); |
|||
}, |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_MENU_ITEM_EDIT_FORM_PROPS = DEFAULT_MENU_ITEM_CREATE_FORM_PROPS; |
|||
@ -0,0 +1,15 @@ |
|||
import { MenuItemDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ToolbarAction } from '@abp/ng.components/extensible'; |
|||
import { MenuItemListComponent } from '../../components/menus/menu-item-list/menu-item-list.component'; |
|||
|
|||
export const DEFAULT_MENU_ITEM_TOOLBAR_ACTIONS = ToolbarAction.createMany<MenuItemDto[]>([ |
|||
{ |
|||
text: 'CmsKit::NewMenuItem', |
|||
action: data => { |
|||
const component = data.getInjected(MenuItemListComponent); |
|||
component.add(); |
|||
}, |
|||
permission: 'CmsKit.Menus.Create', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,2 @@ |
|||
export * from './default-menu-item-create-form-props'; |
|||
export * from './default-menu-item-toolbar-actions'; |
|||
@ -0,0 +1,51 @@ |
|||
import { Validators } from '@angular/forms'; |
|||
import { of } from 'rxjs'; |
|||
import { CreatePageInputDto, UpdatePageInputDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { FormProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { pageStatusOptions } from '@abp/ng.cms-kit/proxy'; |
|||
import { LAYOUT_CONSTANTS } from './layout-constants'; |
|||
|
|||
export const DEFAULT_PAGE_CREATE_FORM_PROPS = FormProp.createMany<CreatePageInputDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'title', |
|||
displayName: 'CmsKit::Title', |
|||
id: 'title', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
id: 'slug', |
|||
validators: () => [Validators.required], |
|||
tooltip: { |
|||
text: 'CmsKit::PageSlugInformation', |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.Enum, |
|||
name: 'layoutName', |
|||
displayName: 'CmsKit::SelectLayout', |
|||
id: 'layoutName', |
|||
options: () => |
|||
of( |
|||
Object.values(LAYOUT_CONSTANTS).map(layout => ({ |
|||
key: layout, |
|||
value: layout.toUpperCase(), |
|||
})), |
|||
), |
|||
validators: () => [Validators.required], |
|||
}, |
|||
{ |
|||
type: ePropType.Enum, |
|||
name: 'status', |
|||
displayName: 'CmsKit::Status', |
|||
id: 'status', |
|||
options: () => of(pageStatusOptions), |
|||
validators: () => [Validators.required], |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_PAGE_EDIT_FORM_PROPS: FormProp<UpdatePageInputDto>[] = |
|||
DEFAULT_PAGE_CREATE_FORM_PROPS; |
|||
@ -0,0 +1,32 @@ |
|||
import { Router } from '@angular/router'; |
|||
import { EntityAction } from '@abp/ng.components/extensible'; |
|||
import { PageDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { PageListComponent } from '../../components/pages/page-list/page-list.component'; |
|||
|
|||
export const DEFAULT_PAGE_ENTITY_ACTIONS = EntityAction.createMany<PageDto>([ |
|||
{ |
|||
text: 'AbpUi::Edit', |
|||
action: data => { |
|||
const router = data.getInjected(Router); |
|||
router.navigate(['/cms/pages/update', data.record.id]); |
|||
}, |
|||
permission: 'CmsKit.Pages.Update', |
|||
}, |
|||
{ |
|||
text: 'AbpUi::Delete', |
|||
action: data => { |
|||
const component = data.getInjected(PageListComponent); |
|||
component.delete(data.record.id!); |
|||
}, |
|||
permission: 'CmsKit.Pages.Delete', |
|||
}, |
|||
{ |
|||
text: 'CmsKit::SetAsHomePage', |
|||
action: data => { |
|||
const component = data.getInjected(PageListComponent); |
|||
const { id, isHomePage } = data.record; |
|||
component.setAsHomePage(id!, isHomePage!); |
|||
}, |
|||
permission: 'CmsKit.Pages.SetAsHomePage', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,62 @@ |
|||
import { of } from 'rxjs'; |
|||
import { PageDto, PageStatus } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
|
|||
export const DEFAULT_PAGE_ENTITY_PROPS = EntityProp.createMany<PageDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'title', |
|||
displayName: 'CmsKit::Title', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'slug', |
|||
displayName: 'CmsKit::Slug', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'status', |
|||
displayName: 'CmsKit::Status', |
|||
sortable: true, |
|||
columnWidth: 120, |
|||
valueResolver: data => { |
|||
const localization = data.getInjected(LocalizationService); |
|||
let result = ''; |
|||
switch (data.record.status) { |
|||
case PageStatus.Draft: |
|||
result = localization.instant('CmsKit::Enum:PageStatus:0'); |
|||
break; |
|||
case PageStatus.Publish: |
|||
result = localization.instant('CmsKit::Enum:PageStatus:1'); |
|||
break; |
|||
} |
|||
return of(result); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.Boolean, |
|||
name: 'isHomePage', |
|||
displayName: 'CmsKit::IsHomePage', |
|||
sortable: true, |
|||
columnWidth: 120, |
|||
}, |
|||
{ |
|||
type: ePropType.Date, |
|||
name: 'creationTime', |
|||
displayName: 'AbpIdentity::CreationTime', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.Date, |
|||
name: 'lastModificationTime', |
|||
displayName: 'AbpIdentity::LastModificationTime', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
]); |
|||
@ -0,0 +1,15 @@ |
|||
import { Router } from '@angular/router'; |
|||
import { PageDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ToolbarAction } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_PAGE_TOOLBAR_ACTIONS = ToolbarAction.createMany<PageDto[]>([ |
|||
{ |
|||
text: 'CmsKit::NewPage', |
|||
action: data => { |
|||
const router = data.getInjected(Router); |
|||
router.navigate(['/cms/pages/create']); |
|||
}, |
|||
permission: 'CmsKit.Pages.Create', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,5 @@ |
|||
export * from './default-page-entity-actions'; |
|||
export * from './default-page-entity-props'; |
|||
export * from './default-page-toolbar-actions'; |
|||
export * from './default-page-create-form-props'; |
|||
export * from './layout-constants'; |
|||
@ -0,0 +1,8 @@ |
|||
import { eLayoutType } from '@abp/ng.core'; |
|||
|
|||
export const LAYOUT_CONSTANTS = { |
|||
Account: eLayoutType.account, |
|||
Public: 'public', |
|||
Empty: eLayoutType.empty, |
|||
Application: eLayoutType.application, |
|||
} as const; |
|||
@ -0,0 +1,36 @@ |
|||
import { TagCreateDto, TagAdminService, TagDefinitionDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { FormProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { Validators } from '@angular/forms'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
export const DEFAULT_TAG_CREATE_FORM_PROPS = FormProp.createMany<TagCreateDto>([ |
|||
{ |
|||
type: ePropType.Enum, |
|||
name: 'entityType', |
|||
displayName: 'CmsKit::EntityType', |
|||
id: 'entityType', |
|||
validators: () => [Validators.required], |
|||
options: data => { |
|||
const tagService = data.getInjected(TagAdminService); |
|||
return tagService.getTagDefinitions().pipe( |
|||
map((definitions: TagDefinitionDto[]) => |
|||
definitions.map(def => ({ |
|||
key: def.displayName || def.entityType || '', |
|||
value: def.entityType || '', |
|||
})), |
|||
), |
|||
); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'name', |
|||
displayName: 'CmsKit::Name', |
|||
id: 'name', |
|||
validators: () => [Validators.required], |
|||
}, |
|||
]); |
|||
|
|||
export const DEFAULT_TAG_EDIT_FORM_PROPS = DEFAULT_TAG_CREATE_FORM_PROPS.filter( |
|||
prop => prop.name !== 'entityType', |
|||
); |
|||
@ -0,0 +1,23 @@ |
|||
import { TagDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityAction } from '@abp/ng.components/extensible'; |
|||
import { TagListComponent } from '../../components/tags/tag-list/tag-list.component'; |
|||
|
|||
export const DEFAULT_TAG_ENTITY_ACTIONS = EntityAction.createMany<TagDto>([ |
|||
{ |
|||
text: 'AbpUi::Edit', |
|||
action: data => { |
|||
const component = data.getInjected(TagListComponent); |
|||
component.edit(data.record.id!); |
|||
}, |
|||
permission: 'CmsKit.Tags.Update', |
|||
}, |
|||
{ |
|||
text: 'AbpUi::Delete', |
|||
action: data => { |
|||
const component = data.getInjected(TagListComponent); |
|||
const { id, name } = data.record; |
|||
component.delete(id!, name!); |
|||
}, |
|||
permission: 'CmsKit.Tags.Delete', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,19 @@ |
|||
import { TagDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { EntityProp, ePropType } from '@abp/ng.components/extensible'; |
|||
|
|||
export const DEFAULT_TAG_ENTITY_PROPS = EntityProp.createMany<TagDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'entityType', |
|||
displayName: 'CmsKit::EntityType', |
|||
sortable: true, |
|||
columnWidth: 200, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'name', |
|||
displayName: 'CmsKit::Name', |
|||
sortable: true, |
|||
columnWidth: 250, |
|||
}, |
|||
]); |
|||
@ -0,0 +1,15 @@ |
|||
import { TagDto } from '@abp/ng.cms-kit/proxy'; |
|||
import { ToolbarAction } from '@abp/ng.components/extensible'; |
|||
import { TagListComponent } from '../../components/tags/tag-list/tag-list.component'; |
|||
|
|||
export const DEFAULT_TAG_TOOLBAR_ACTIONS = ToolbarAction.createMany<TagDto[]>([ |
|||
{ |
|||
text: 'CmsKit::NewTag', |
|||
action: data => { |
|||
const component = data.getInjected(TagListComponent); |
|||
component.add(); |
|||
}, |
|||
permission: 'CmsKit.Tags.Create', |
|||
icon: 'fa fa-plus', |
|||
}, |
|||
]); |
|||
@ -0,0 +1,4 @@ |
|||
export * from './default-tag-entity-actions'; |
|||
export * from './default-tag-entity-props'; |
|||
export * from './default-tag-toolbar-actions'; |
|||
export * from './default-tag-create-form-props'; |
|||
@ -0,0 +1,18 @@ |
|||
export enum eCmsKitAdminComponents { |
|||
CommentList = 'CmsKit.Admin.CommentList', |
|||
CommentDetails = 'CmsKit.Admin.CommentDetails', |
|||
|
|||
Tags = 'CmsKit.Admin.Tags', |
|||
|
|||
Pages = 'CmsKit.Admin.Pages', |
|||
PageForm = 'CmsKit.Admin.PageForm', |
|||
|
|||
Blogs = 'CmsKit.Admin.Blogs', |
|||
|
|||
BlogPosts = 'CmsKit.Admin.BlogPosts', |
|||
BlogPostForm = 'CmsKit.Admin.BlogPostForm', |
|||
|
|||
Menus = 'CmsKit.Admin.Menus', |
|||
|
|||
GlobalResources = 'CmsKit.Admin.GlobalResources', |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './components'; |
|||
@ -0,0 +1,76 @@ |
|||
import { eCmsKitAdminComponents } from '../enums'; |
|||
import { |
|||
EntityActionContributorCallback, |
|||
EntityPropContributorCallback, |
|||
ToolbarActionContributorCallback, |
|||
CreateFormPropContributorCallback, |
|||
EditFormPropContributorCallback, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { |
|||
CommentWithAuthorDto, |
|||
TagDto, |
|||
PageDto, |
|||
BlogDto, |
|||
BlogPostListDto, |
|||
MenuItemDto, |
|||
MenuItemCreateInput, |
|||
MenuItemUpdateInput, |
|||
CreatePageInputDto, |
|||
UpdatePageInputDto, |
|||
CreateBlogDto, |
|||
CreateBlogPostDto, |
|||
UpdateBlogDto, |
|||
UpdateBlogPostDto, |
|||
TagCreateDto, |
|||
TagUpdateDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
export type CmsKitAdminEntityActionContributors = Partial<{ |
|||
[eCmsKitAdminComponents.CommentList]: EntityActionContributorCallback<CommentWithAuthorDto>[]; |
|||
[eCmsKitAdminComponents.CommentDetails]: EntityActionContributorCallback<CommentWithAuthorDto>[]; |
|||
[eCmsKitAdminComponents.Tags]: EntityActionContributorCallback<TagDto>[]; |
|||
[eCmsKitAdminComponents.Pages]: EntityActionContributorCallback<PageDto>[]; |
|||
[eCmsKitAdminComponents.Blogs]: EntityActionContributorCallback<BlogDto>[]; |
|||
[eCmsKitAdminComponents.BlogPosts]: EntityActionContributorCallback<BlogPostListDto>[]; |
|||
}>; |
|||
|
|||
export type CmsKitAdminEntityPropContributors = Partial<{ |
|||
[eCmsKitAdminComponents.CommentList]: EntityPropContributorCallback<CommentWithAuthorDto>[]; |
|||
[eCmsKitAdminComponents.CommentDetails]: EntityPropContributorCallback<CommentWithAuthorDto>[]; |
|||
[eCmsKitAdminComponents.Tags]: EntityPropContributorCallback<TagDto>[]; |
|||
[eCmsKitAdminComponents.Pages]: EntityPropContributorCallback<PageDto>[]; |
|||
[eCmsKitAdminComponents.Blogs]: EntityPropContributorCallback<BlogDto>[]; |
|||
[eCmsKitAdminComponents.BlogPosts]: EntityPropContributorCallback<BlogPostListDto>[]; |
|||
}>; |
|||
|
|||
export type CmsKitAdminToolbarActionContributors = Partial<{ |
|||
[eCmsKitAdminComponents.Tags]: ToolbarActionContributorCallback<TagDto[]>[]; |
|||
[eCmsKitAdminComponents.Pages]: ToolbarActionContributorCallback<PageDto[]>[]; |
|||
[eCmsKitAdminComponents.Blogs]: ToolbarActionContributorCallback<BlogDto[]>[]; |
|||
[eCmsKitAdminComponents.BlogPosts]: ToolbarActionContributorCallback<BlogPostListDto[]>[]; |
|||
[eCmsKitAdminComponents.Menus]: ToolbarActionContributorCallback<MenuItemDto[]>[]; |
|||
}>; |
|||
|
|||
export type CmsKitAdminCreateFormPropContributors = Partial<{ |
|||
[eCmsKitAdminComponents.Tags]: CreateFormPropContributorCallback<TagCreateDto>[]; |
|||
[eCmsKitAdminComponents.PageForm]: CreateFormPropContributorCallback<CreatePageInputDto>[]; |
|||
[eCmsKitAdminComponents.Blogs]: CreateFormPropContributorCallback<CreateBlogDto>[]; |
|||
[eCmsKitAdminComponents.BlogPostForm]: CreateFormPropContributorCallback<CreateBlogPostDto>[]; |
|||
[eCmsKitAdminComponents.Menus]: CreateFormPropContributorCallback<MenuItemCreateInput>[]; |
|||
}>; |
|||
|
|||
export type CmsKitAdminEditFormPropContributors = Partial<{ |
|||
[eCmsKitAdminComponents.Tags]: EditFormPropContributorCallback<TagUpdateDto>[]; |
|||
[eCmsKitAdminComponents.PageForm]: EditFormPropContributorCallback<UpdatePageInputDto>[]; |
|||
[eCmsKitAdminComponents.Blogs]: EditFormPropContributorCallback<UpdateBlogDto>[]; |
|||
[eCmsKitAdminComponents.BlogPostForm]: EditFormPropContributorCallback<UpdateBlogPostDto>[]; |
|||
[eCmsKitAdminComponents.Menus]: EditFormPropContributorCallback<MenuItemUpdateInput>[]; |
|||
}>; |
|||
|
|||
export interface CmsKitAdminConfigOptions { |
|||
entityActionContributors?: CmsKitAdminEntityActionContributors; |
|||
entityPropContributors?: CmsKitAdminEntityPropContributors; |
|||
toolbarActionContributors?: CmsKitAdminToolbarActionContributors; |
|||
createFormPropContributors?: CmsKitAdminCreateFormPropContributors; |
|||
editFormPropContributors?: CmsKitAdminEditFormPropContributors; |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './config-options'; |
|||
@ -0,0 +1,6 @@ |
|||
export * from './components'; |
|||
export * from './defaults'; |
|||
export * from './enums'; |
|||
export * from './resolvers'; |
|||
export * from './tokens'; |
|||
export * from './cms-kit-admin.routes'; |
|||
@ -0,0 +1,81 @@ |
|||
import { |
|||
ExtensionsService, |
|||
getObjectExtensionEntitiesFromStore, |
|||
mapEntitiesToContributors, |
|||
mergeWithDefaultActions, |
|||
mergeWithDefaultProps, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { inject, Injector } from '@angular/core'; |
|||
import { ResolveFn } from '@angular/router'; |
|||
import { map, tap } from 'rxjs'; |
|||
import { eCmsKitAdminComponents } from '../enums'; |
|||
import { |
|||
CMS_KIT_ADMIN_ENTITY_ACTION_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_TOOLBAR_ACTION_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_ENTITY_PROP_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_CREATE_FORM_PROP_CONTRIBUTORS, |
|||
CMS_KIT_ADMIN_EDIT_FORM_PROP_CONTRIBUTORS, |
|||
DEFAULT_CMS_KIT_ADMIN_ENTITY_ACTIONS, |
|||
DEFAULT_CMS_KIT_ADMIN_TOOLBAR_ACTIONS, |
|||
DEFAULT_CMS_KIT_ADMIN_ENTITY_PROPS, |
|||
DEFAULT_CMS_KIT_ADMIN_CREATE_FORM_PROPS, |
|||
DEFAULT_CMS_KIT_ADMIN_EDIT_FORM_PROPS, |
|||
} from '../tokens'; |
|||
|
|||
export const cmsKitAdminExtensionsResolver: ResolveFn<any> = () => { |
|||
const injector = inject(Injector); |
|||
const extensions = inject(ExtensionsService); |
|||
|
|||
const config = { optional: true }; |
|||
|
|||
const actionContributors = inject(CMS_KIT_ADMIN_ENTITY_ACTION_CONTRIBUTORS, config) || {}; |
|||
const toolbarContributors = inject(CMS_KIT_ADMIN_TOOLBAR_ACTION_CONTRIBUTORS, config) || {}; |
|||
const propContributors = inject(CMS_KIT_ADMIN_ENTITY_PROP_CONTRIBUTORS, config) || {}; |
|||
const createFormContributors = inject(CMS_KIT_ADMIN_CREATE_FORM_PROP_CONTRIBUTORS, config) || {}; |
|||
const editFormContributors = inject(CMS_KIT_ADMIN_EDIT_FORM_PROP_CONTRIBUTORS, config) || {}; |
|||
|
|||
return getObjectExtensionEntitiesFromStore(injector, 'CmsKit').pipe( |
|||
map(entities => ({ |
|||
[eCmsKitAdminComponents.CommentList]: entities.Comment, |
|||
[eCmsKitAdminComponents.CommentDetails]: entities.Comment, |
|||
[eCmsKitAdminComponents.Tags]: entities.Tag, |
|||
[eCmsKitAdminComponents.Pages]: entities.Page, |
|||
[eCmsKitAdminComponents.PageForm]: entities.Page, |
|||
[eCmsKitAdminComponents.Blogs]: entities.Blog, |
|||
[eCmsKitAdminComponents.BlogPosts]: entities.BlogPost, |
|||
[eCmsKitAdminComponents.BlogPostForm]: entities.BlogPost, |
|||
[eCmsKitAdminComponents.Menus]: entities.MenuItem, |
|||
})), |
|||
mapEntitiesToContributors(injector, 'CmsKit'), |
|||
tap(objectExtensionContributors => { |
|||
mergeWithDefaultActions( |
|||
extensions.entityActions, |
|||
DEFAULT_CMS_KIT_ADMIN_ENTITY_ACTIONS, |
|||
actionContributors, |
|||
); |
|||
mergeWithDefaultActions( |
|||
extensions.toolbarActions, |
|||
DEFAULT_CMS_KIT_ADMIN_TOOLBAR_ACTIONS, |
|||
toolbarContributors, |
|||
); |
|||
mergeWithDefaultProps( |
|||
extensions.entityProps, |
|||
DEFAULT_CMS_KIT_ADMIN_ENTITY_PROPS, |
|||
objectExtensionContributors.prop, |
|||
propContributors, |
|||
); |
|||
mergeWithDefaultProps( |
|||
extensions.createFormProps, |
|||
DEFAULT_CMS_KIT_ADMIN_CREATE_FORM_PROPS, |
|||
objectExtensionContributors.createForm, |
|||
createFormContributors, |
|||
); |
|||
mergeWithDefaultProps( |
|||
extensions.editFormProps, |
|||
DEFAULT_CMS_KIT_ADMIN_EDIT_FORM_PROPS, |
|||
objectExtensionContributors.editForm, |
|||
editFormContributors, |
|||
); |
|||
}), |
|||
); |
|||
}; |
|||
@ -0,0 +1 @@ |
|||
export * from './extensions.resolver'; |
|||
@ -0,0 +1,91 @@ |
|||
import { Injectable, inject } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormGroup } from '@angular/forms'; |
|||
import { Observable } from 'rxjs'; |
|||
import { tap } from 'rxjs/operators'; |
|||
import { ToasterService } from '@abp/ng.theme.shared'; |
|||
import { |
|||
BlogPostAdminService, |
|||
CreateBlogPostDto, |
|||
UpdateBlogPostDto, |
|||
BlogPostDto, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class BlogPostFormService { |
|||
private blogPostService = inject(BlogPostAdminService); |
|||
private toasterService = inject(ToasterService); |
|||
private router = inject(Router); |
|||
|
|||
create(form: FormGroup): Observable<BlogPostDto> { |
|||
if (!form.valid) { |
|||
throw new Error('Form is invalid'); |
|||
} |
|||
|
|||
return this.blogPostService.create(form.value as CreateBlogPostDto).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/blog-posts']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
createAsDraft(form: FormGroup): Observable<BlogPostDto> { |
|||
if (!form.valid) { |
|||
throw new Error('Form is invalid'); |
|||
} |
|||
|
|||
return this.blogPostService.create(form.value as CreateBlogPostDto).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/blog-posts']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
createAndPublish(form: FormGroup): Observable<BlogPostDto> { |
|||
if (!form.valid) { |
|||
throw new Error('Form is invalid'); |
|||
} |
|||
|
|||
return this.blogPostService.createAndPublish(form.value as CreateBlogPostDto).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/blog-posts']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
createAndSendToReview(form: FormGroup): Observable<BlogPostDto> { |
|||
if (!form.valid) { |
|||
throw new Error('Form is invalid'); |
|||
} |
|||
|
|||
return this.blogPostService.createAndSendToReview(form.value as CreateBlogPostDto).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/blog-posts']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
update(blogPostId: string, form: FormGroup, blogPost: BlogPostDto): Observable<BlogPostDto> { |
|||
if (!form.valid || !blogPost) { |
|||
throw new Error('Form is invalid or blog post is missing'); |
|||
} |
|||
|
|||
const formValue = { |
|||
...blogPost, |
|||
...form.value, |
|||
} as UpdateBlogPostDto; |
|||
|
|||
return this.blogPostService.update(blogPostId, formValue).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/blog-posts']); |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
import { inject, Injectable } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { tap } from 'rxjs/operators'; |
|||
import { ConfigStateService, ListService } from '@abp/ng.core'; |
|||
import { Confirmation, ConfirmationService, ToasterService } from '@abp/ng.theme.shared'; |
|||
import { CommentAdminService, CommentGetListInput } from '@abp/ng.cms-kit/proxy'; |
|||
import { CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT } from '../components'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class CommentEntityService { |
|||
private commentService = inject(CommentAdminService); |
|||
private toasterService = inject(ToasterService); |
|||
private confirmation = inject(ConfirmationService); |
|||
private configState = inject(ConfigStateService); |
|||
private router = inject(Router); |
|||
|
|||
get requireApprovement(): boolean { |
|||
return ( |
|||
this.configState.getSetting(CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT).toLowerCase() === 'true' |
|||
); |
|||
} |
|||
|
|||
isCommentReply(commentId: string | undefined): boolean { |
|||
if (!commentId) { |
|||
return false; |
|||
} |
|||
|
|||
const id = this.router.url.split('/').pop(); |
|||
return id === commentId; |
|||
} |
|||
|
|||
updateApprovalStatus(id: string, isApproved: boolean, list: ListService<CommentGetListInput>) { |
|||
this.commentService |
|||
.updateApprovalStatus(id, { isApproved: isApproved }) |
|||
.pipe(tap(() => list.get())) |
|||
.subscribe(() => |
|||
isApproved |
|||
? this.toasterService.success('CmsKit::ApprovedSuccessfully') |
|||
: this.toasterService.success('CmsKit::ApprovalRevokedSuccessfully'), |
|||
); |
|||
} |
|||
|
|||
delete(id: string, list: ListService<CommentGetListInput>) { |
|||
this.confirmation |
|||
.warn('CmsKit::CommentDeletionConfirmationMessage', 'AbpUi::AreYouSure', { |
|||
yesText: 'AbpUi::Yes', |
|||
cancelText: 'AbpUi::Cancel', |
|||
}) |
|||
.subscribe(status => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.commentService.delete(id).subscribe(() => { |
|||
list.get(); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './page-form.service'; |
|||
export * from './blog-post-form.service'; |
|||
export * from './comment-entity.service'; |
|||
@ -0,0 +1,119 @@ |
|||
import { Injectable, inject } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormGroup } from '@angular/forms'; |
|||
import { Observable } from 'rxjs'; |
|||
import { tap } from 'rxjs/operators'; |
|||
import { ToasterService } from '@abp/ng.theme.shared'; |
|||
import { |
|||
PageAdminService, |
|||
CreatePageInputDto, |
|||
UpdatePageInputDto, |
|||
PageDto, |
|||
PageStatus, |
|||
} from '@abp/ng.cms-kit/proxy'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class PageFormService { |
|||
private pageService = inject(PageAdminService); |
|||
private toasterService = inject(ToasterService); |
|||
private router = inject(Router); |
|||
|
|||
create(form: FormGroup): Observable<PageDto> { |
|||
if (!form.valid) { |
|||
throw new Error('Form is invalid'); |
|||
} |
|||
|
|||
return this.pageService.create(form.value as CreatePageInputDto).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/pages']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
createAsDraft(form: FormGroup): Observable<PageDto> { |
|||
if (!form.valid) { |
|||
throw new Error('Form is invalid'); |
|||
} |
|||
|
|||
const formValue = { ...form.value, status: PageStatus.Draft } as CreatePageInputDto; |
|||
return this.pageService.create(formValue).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/pages']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
publish(form: FormGroup): Observable<PageDto> { |
|||
if (!form.valid) { |
|||
throw new Error('Form is invalid'); |
|||
} |
|||
|
|||
const formValue = { ...form.value, status: PageStatus.Publish } as CreatePageInputDto; |
|||
return this.pageService.create(formValue).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/pages']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
update(pageId: string, form: FormGroup, page: PageDto): Observable<PageDto> { |
|||
if (!form.valid || !page) { |
|||
throw new Error('Form is invalid or page is missing'); |
|||
} |
|||
|
|||
const formValue = { |
|||
...page, |
|||
...form.value, |
|||
} as UpdatePageInputDto; |
|||
|
|||
return this.pageService.update(pageId, formValue).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/pages']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
updateAsDraft(pageId: string, form: FormGroup, page: PageDto): Observable<PageDto> { |
|||
if (!form.valid || !page) { |
|||
throw new Error('Form is invalid or page is missing'); |
|||
} |
|||
|
|||
const formValue = { |
|||
...page, |
|||
...form.value, |
|||
status: PageStatus.Draft, |
|||
} as UpdatePageInputDto; |
|||
|
|||
return this.pageService.update(pageId, formValue).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/pages']); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
updateAndPublish(pageId: string, form: FormGroup, page: PageDto): Observable<PageDto> { |
|||
if (!form.valid || !page) { |
|||
throw new Error('Form is invalid or page is missing'); |
|||
} |
|||
|
|||
const formValue = { |
|||
...page, |
|||
...form.value, |
|||
status: PageStatus.Publish, |
|||
} as UpdatePageInputDto; |
|||
|
|||
return this.pageService.update(pageId, formValue).pipe( |
|||
tap(() => { |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.router.navigate(['/cms/pages']); |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
/* eslint-disable */ |
|||
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; |
|||
import { FormGroup } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { of } from 'rxjs'; |
|||
// @ts-ignore - test types are resolved only in the library build context
|
|||
import { ToasterService } from '@abp/ng.theme.shared'; |
|||
// @ts-ignore - test types are resolved only in the library build context
|
|||
import { BlogPostAdminService } from '@abp/ng.cms-kit/proxy'; |
|||
import { BlogPostFormService } from '../services'; |
|||
|
|||
describe('BlogPostFormService', () => { |
|||
let service: BlogPostFormService; |
|||
let blogPostAdminService: any; |
|||
let toasterService: any; |
|||
let router: any; |
|||
|
|||
beforeEach(() => { |
|||
blogPostAdminService = { |
|||
create: jest.fn().mockReturnValue(of({})), |
|||
createAndPublish: jest.fn().mockReturnValue(of({})), |
|||
createAndSendToReview: jest.fn().mockReturnValue(of({})), |
|||
update: jest.fn().mockReturnValue(of({})), |
|||
}; |
|||
|
|||
toasterService = { |
|||
success: jest.fn(), |
|||
}; |
|||
|
|||
router = { |
|||
navigate: jest.fn(), |
|||
}; |
|||
|
|||
TestBed.configureTestingModule({ |
|||
providers: [ |
|||
BlogPostFormService, |
|||
{ provide: BlogPostAdminService, useValue: blogPostAdminService }, |
|||
{ provide: ToasterService, useValue: toasterService }, |
|||
{ provide: Router, useValue: router }, |
|||
], |
|||
}); |
|||
|
|||
service = TestBed.inject(BlogPostFormService); |
|||
}); |
|||
|
|||
function createValidForm(): FormGroup { |
|||
// We don't rely on any specific controls, only on form.value and validity.
|
|||
return new FormGroup({}); |
|||
} |
|||
|
|||
function createInvalidForm(): FormGroup { |
|||
const form = new FormGroup({}); |
|||
form.setErrors({ invalid: true }); |
|||
return form; |
|||
} |
|||
|
|||
it('should throw when creating with invalid form', () => { |
|||
const form = createInvalidForm(); |
|||
|
|||
expect(() => service.create(form)).toThrowError('Form is invalid'); |
|||
}); |
|||
|
|||
it('should call BlogPostAdminService.create and navigate on create', done => { |
|||
const form = createValidForm(); |
|||
|
|||
service.create(form).subscribe({ |
|||
next: () => { |
|||
expect(blogPostAdminService.create).toHaveBeenCalledWith(form.value); |
|||
expect(toasterService.success).toHaveBeenCalledWith('AbpUi::SavedSuccessfully'); |
|||
expect(router.navigate).toHaveBeenCalledWith(['/cms/blog-posts']); |
|||
done(); |
|||
}, |
|||
error: err => done(err as any), |
|||
}); |
|||
}); |
|||
|
|||
it('should call BlogPostAdminService.create on createAsDraft', done => { |
|||
const form = createValidForm(); |
|||
|
|||
service.createAsDraft(form).subscribe({ |
|||
next: () => { |
|||
expect(blogPostAdminService.create).toHaveBeenCalledWith(form.value); |
|||
done(); |
|||
}, |
|||
error: err => done(err as any), |
|||
}); |
|||
}); |
|||
|
|||
it('should call BlogPostAdminService.createAndPublish on createAndPublish', done => { |
|||
const form = createValidForm(); |
|||
|
|||
service.createAndPublish(form).subscribe({ |
|||
next: () => { |
|||
expect(blogPostAdminService.createAndPublish).toHaveBeenCalledWith(form.value); |
|||
done(); |
|||
}, |
|||
error: err => done(err as any), |
|||
}); |
|||
}); |
|||
|
|||
it('should call BlogPostAdminService.createAndSendToReview on createAndSendToReview', done => { |
|||
const form = createValidForm(); |
|||
|
|||
service.createAndSendToReview(form).subscribe({ |
|||
next: () => { |
|||
expect(blogPostAdminService.createAndSendToReview).toHaveBeenCalledWith(form.value); |
|||
done(); |
|||
}, |
|||
error: err => done(err as any), |
|||
}); |
|||
}); |
|||
|
|||
it('should throw when updating with invalid form or missing blog post', () => { |
|||
const form = createInvalidForm(); |
|||
|
|||
expect(() => service.update('id', form, {} as any)).toThrowError( |
|||
'Form is invalid or blog post is missing', |
|||
); |
|||
|
|||
const validForm = createValidForm(); |
|||
expect(() => service.update('id', validForm, null as any)).toThrowError( |
|||
'Form is invalid or blog post is missing', |
|||
); |
|||
}); |
|||
|
|||
it('should call BlogPostAdminService.update and navigate on update', done => { |
|||
const form = createValidForm(); |
|||
const blogPost = { id: '1', title: 't' }; |
|||
|
|||
service.update('1', form, blogPost).subscribe({ |
|||
next: () => { |
|||
expect(blogPostAdminService.update).toHaveBeenCalled(); |
|||
expect(toasterService.success).toHaveBeenCalledWith('AbpUi::SavedSuccessfully'); |
|||
expect(router.navigate).toHaveBeenCalledWith(['/cms/blog-posts']); |
|||
done(); |
|||
}, |
|||
error: err => done(err as any), |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,64 @@ |
|||
/* eslint-disable */ |
|||
import { describe, it, expect } from '@jest/globals'; |
|||
import { Routes } from '@angular/router'; |
|||
import { createRoutes } from '../cms-kit-admin.routes'; |
|||
import { CmsKitAdminConfigOptions } from '../models'; |
|||
|
|||
describe('cms-kit-admin routes', () => { |
|||
function findRoute(routes: Routes, path: string): any { |
|||
for (const route of routes) { |
|||
if (route.path === path) { |
|||
return route; |
|||
} |
|||
if (route.children) { |
|||
const found = findRoute(route.children, path); |
|||
if (found) { |
|||
return found; |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
it('should create base route with children', () => { |
|||
const routes = createRoutes(); |
|||
|
|||
expect(Array.isArray(routes)).toBe(true); |
|||
const root = routes[0]; |
|||
expect(root.path).toBe(''); |
|||
expect(root.children?.length).toBeGreaterThan(0); |
|||
}); |
|||
|
|||
it('should contain expected admin routes with required policies', () => { |
|||
const routes = createRoutes(); |
|||
|
|||
const comments = findRoute(routes, 'comments'); |
|||
const pages = findRoute(routes, 'pages'); |
|||
const blogs = findRoute(routes, 'blogs'); |
|||
const blogPosts = findRoute(routes, 'blog-posts'); |
|||
const menus = findRoute(routes, 'menus'); |
|||
const globalResources = findRoute(routes, 'global-resources'); |
|||
|
|||
expect(comments?.data?.requiredPolicy).toBe('CmsKit.Comments'); |
|||
expect(pages?.data?.requiredPolicy).toBe('CmsKit.Pages'); |
|||
expect(blogs?.data?.requiredPolicy).toBe('CmsKit.Blogs'); |
|||
expect(blogPosts?.data?.requiredPolicy).toBe('CmsKit.BlogPosts'); |
|||
expect(menus?.data?.requiredPolicy).toBe('CmsKit.Menus'); |
|||
expect(globalResources?.data?.requiredPolicy).toBe('CmsKit.GlobalResources'); |
|||
}); |
|||
|
|||
it('should propagate contributors from config options', () => { |
|||
const options: CmsKitAdminConfigOptions = { |
|||
entityActionContributors: {}, |
|||
entityPropContributors: {}, |
|||
toolbarActionContributors: {}, |
|||
createFormPropContributors: {}, |
|||
editFormPropContributors: {}, |
|||
}; |
|||
|
|||
const routes = createRoutes(options); |
|||
const root = routes[0]; |
|||
|
|||
expect(root.providers).toBeDefined(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,102 @@ |
|||
/* eslint-disable */ |
|||
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; |
|||
import { Router } from '@angular/router'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { of, Subject } from 'rxjs'; |
|||
// @ts-ignore - test types are resolved only in the library build context
|
|||
import { ConfigStateService, ListService } from '@abp/ng.core'; |
|||
// @ts-ignore - test types are resolved only in the library build context
|
|||
import { Confirmation, ConfirmationService, ToasterService } from '@abp/ng.theme.shared'; |
|||
// @ts-ignore - proxy module types are resolved only in the library build context
|
|||
import { CommentAdminService, CommentGetListInput } from '@abp/ng.cms-kit/proxy'; |
|||
import { CommentEntityService } from '../services'; |
|||
|
|||
describe('CommentEntityService', () => { |
|||
let service: CommentEntityService; |
|||
let commentAdminService: any; |
|||
let toasterService: any; |
|||
let confirmationService: any; |
|||
let configStateService: any; |
|||
let router: any; |
|||
|
|||
beforeEach(() => { |
|||
commentAdminService = { |
|||
updateApprovalStatus: jest.fn().mockReturnValue(of(void 0)), |
|||
delete: jest.fn().mockReturnValue(of(void 0)), |
|||
}; |
|||
|
|||
toasterService = { |
|||
success: jest.fn(), |
|||
}; |
|||
|
|||
confirmationService = { |
|||
warn: jest.fn(), |
|||
}; |
|||
|
|||
configStateService = { |
|||
getSetting: jest.fn(), |
|||
}; |
|||
|
|||
router = { |
|||
url: '/cms/comments/123', |
|||
}; |
|||
|
|||
TestBed.configureTestingModule({ |
|||
providers: [ |
|||
CommentEntityService, |
|||
{ provide: CommentAdminService, useValue: commentAdminService }, |
|||
{ provide: ToasterService, useValue: toasterService }, |
|||
{ provide: ConfirmationService, useValue: confirmationService }, |
|||
{ provide: ConfigStateService, useValue: configStateService }, |
|||
{ provide: Router, useValue: router }, |
|||
], |
|||
}); |
|||
|
|||
service = TestBed.inject(CommentEntityService); |
|||
}); |
|||
|
|||
it('should return requireApprovement based on setting', () => { |
|||
configStateService.getSetting.mockReturnValue('true'); |
|||
expect(service.requireApprovement).toBe(true); |
|||
|
|||
configStateService.getSetting.mockReturnValue('false'); |
|||
expect(service.requireApprovement).toBe(false); |
|||
}); |
|||
|
|||
it('should detect comment reply from router url', () => { |
|||
expect(service.isCommentReply('123')).toBe(true); |
|||
expect(service.isCommentReply('456')).toBe(false); |
|||
expect(service.isCommentReply(undefined)).toBe(false); |
|||
}); |
|||
|
|||
it('should update approval status and refresh list', () => { |
|||
const list = { |
|||
get: jest.fn(), |
|||
} as unknown as ListService<any>; |
|||
|
|||
service.updateApprovalStatus('1', true, list); |
|||
|
|||
expect(commentAdminService.updateApprovalStatus).toHaveBeenCalledWith('1', { |
|||
isApproved: true, |
|||
}); |
|||
expect(list.get).toHaveBeenCalled(); |
|||
expect(toasterService.success).toHaveBeenCalledWith('CmsKit::ApprovedSuccessfully'); |
|||
}); |
|||
|
|||
it('should show confirmation and delete comment when confirmed', () => { |
|||
const subject = new Subject<Confirmation.Status>(); |
|||
(confirmationService.warn as jest.Mock).mockReturnValue(subject.asObservable()); |
|||
|
|||
const list = { |
|||
get: jest.fn(), |
|||
} as unknown as ListService<CommentGetListInput>; |
|||
|
|||
service.delete('1', list); |
|||
|
|||
subject.next(Confirmation.Status.confirm); |
|||
subject.complete(); |
|||
|
|||
expect(commentAdminService.delete).toHaveBeenCalledWith('1'); |
|||
expect(list.get).toHaveBeenCalled(); |
|||
}); |
|||
}); |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue