added command-form directive and logistic
This commit is contained in:
parent
8fefc1a6df
commit
860d48005e
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openharbor/ngx-data-ui-core",
|
||||
"version": "18.0.0-alpha.7",
|
||||
"version": "18.0.0-alpha.15",
|
||||
"repository": "https://git.openharbor.io/Open-Harbor/ngx-data-ui",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
|
@ -1,4 +1,6 @@
|
||||
import {Observable} from "rxjs";
|
||||
import {TemplateRef} from "@angular/core";
|
||||
import {AbstractControl, FormGroup} from "@angular/forms";
|
||||
|
||||
export interface IConfirmOptions {
|
||||
title: string;
|
||||
@ -13,6 +15,15 @@ export interface IConfirmEvents {
|
||||
loading: Observable<boolean>;
|
||||
}
|
||||
|
||||
export interface ICommandDirectiveService<TConfirmOptions extends IConfirmOptions = IConfirmOptions> {
|
||||
confirm(options?: TConfirmOptions & IConfirmEvents): Observable<boolean>;
|
||||
export interface IConfirmForm<TControl extends FormType<TControl> = any> {
|
||||
formTemplate?: TemplateRef<any>;
|
||||
form?: FormGroup<TControl>
|
||||
}
|
||||
|
||||
export type FormType<TControl = any> = { [K in keyof TControl]: AbstractControl<any>; }
|
||||
|
||||
export type ConfirmOptions<TConfirmOptions extends IConfirmOptions> = TConfirmOptions & IConfirmEvents & IConfirmForm;
|
||||
|
||||
export interface ICommandDirectiveService<TConfirmOptions extends IConfirmOptions = IConfirmOptions> {
|
||||
confirm(options?: ConfirmOptions<TConfirmOptions>): Observable<boolean>;
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
import {Directive, inject, Input, TemplateRef} from '@angular/core';
|
||||
import {FormGroup} from "@angular/forms";
|
||||
import {FormType} from "../abstractions/command-directive-service.abstraction";
|
||||
|
||||
@Directive({
|
||||
selector: '[duiCommandForm]',
|
||||
standalone: true
|
||||
})
|
||||
export class CommandFormDirective<TForm extends FormType<TForm>> {
|
||||
public readonly template = inject(TemplateRef<any>);
|
||||
|
||||
@Input({ alias: 'commandForm', required: true })
|
||||
public form!: FormGroup<TForm>;
|
||||
}
|
@ -1,33 +1,36 @@
|
||||
import {Directive, EventEmitter, HostListener, Input, Output} from '@angular/core';
|
||||
import {AfterContentInit, ContentChild, Directive, EventEmitter, HostListener, Input, Output} from '@angular/core';
|
||||
import {IDataSource} from '@openharbor/data';
|
||||
import {finalize} from "rxjs";
|
||||
import {
|
||||
ConfirmOptions, FormType,
|
||||
ICommandDirectiveService,
|
||||
IConfirmEvents,
|
||||
IConfirmOptions
|
||||
} from "../abstractions/command-directive-service.abstraction";
|
||||
import {CommandFormDirective} from "./command-form.directive";
|
||||
|
||||
@Directive({
|
||||
selector: '[duiCommand]',
|
||||
standalone: true
|
||||
})
|
||||
export class CommandDirective<TModel extends {}, TConfirmOptions extends IConfirmOptions> {
|
||||
export class CommandDirective<TModel extends {}, TConfirmOptions extends IConfirmOptions, TForm extends FormType<TForm> = any> implements AfterContentInit {
|
||||
@ContentChild(CommandFormDirective) commandFormTemplate?: CommandFormDirective<TForm>;
|
||||
|
||||
@Input() confirm: boolean = false;
|
||||
@Input() confirmOptions?: TConfirmOptions;
|
||||
@Input() refresh: boolean = true;
|
||||
@Input() params: any;
|
||||
@Input() params?: any;
|
||||
|
||||
@Input() dataSource!: IDataSource<TModel>;
|
||||
@Input() command!: string;
|
||||
@Input() model!: object;
|
||||
@Input({ required: true }) dataSource!: IDataSource<TModel>;
|
||||
@Input({ required: true }) command!: string;
|
||||
@Input() model: object = {};
|
||||
@Input() service?: ICommandDirectiveService;
|
||||
|
||||
@Output() success: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() failure: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() loading: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
|
||||
constructor() {
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
console.log('init');
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
@ -44,14 +47,20 @@ export class CommandDirective<TModel extends {}, TConfirmOptions extends IConfir
|
||||
...{
|
||||
success: this.success.asObservable(),
|
||||
failure: this.failure.asObservable(),
|
||||
loading: this.loading.asObservable()
|
||||
loading: this.loading.asObservable(),
|
||||
},
|
||||
...{
|
||||
formTemplate: this.commandFormTemplate?.template,
|
||||
form: this.commandFormTemplate?.form
|
||||
}
|
||||
} as TConfirmOptions & IConfirmEvents;
|
||||
} as ConfirmOptions<TConfirmOptions>;
|
||||
|
||||
this.service.confirm(options)
|
||||
.subscribe(result => {
|
||||
const model = { ...this.model, ...options.form?.getRawValue() };
|
||||
console.log(options.form?.getRawValue());
|
||||
if (result)
|
||||
this.executeCommand();
|
||||
this.executeCommand(model);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -59,10 +68,10 @@ export class CommandDirective<TModel extends {}, TConfirmOptions extends IConfir
|
||||
this.executeCommand();
|
||||
}
|
||||
|
||||
private executeCommand() {
|
||||
private executeCommand(model?: any) {
|
||||
this.dataSource.resolveCommandModelByName<TModel>({
|
||||
command: this.command,
|
||||
model: this.model,
|
||||
model: model,
|
||||
params: this.params
|
||||
})
|
||||
.subscribe({ next: commandModel => {
|
||||
|
@ -0,0 +1 @@
|
||||
<p>data-grid works!</p>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DataGridComponent } from './data-grid.component';
|
||||
|
||||
describe('DataGridComponent', () => {
|
||||
let component: DataGridComponent;
|
||||
let fixture: ComponentFixture<DataGridComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DataGridComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DataGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'dui-data-grid',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './data-grid.component.html',
|
||||
styleUrl: './data-grid.component.css'
|
||||
})
|
||||
export class DataGridComponent<TModel> {
|
||||
|
||||
@Input() dataSource: IDataSource<TModel>;
|
||||
}
|
@ -4,3 +4,4 @@
|
||||
|
||||
export * from './lib/command/directives/command.directive';
|
||||
export * from './lib/command/abstractions/command-directive-service.abstraction';
|
||||
export * from './lib/command/directives/command-form.directive';
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openharbor/ngx-data-ui-md",
|
||||
"version": "18.0.0-alpha.8",
|
||||
"version": "18.0.0-alpha.24",
|
||||
"repository": "https://git.openharbor.io/Open-Harbor/ngx-data-ui",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {ICommandDirectiveService, IConfirmEvents, IConfirmOptions} from "@openharbor/ngx-data-ui-core";
|
||||
import {ConfirmOptions, ICommandDirectiveService, IConfirmOptions} from "@openharbor/ngx-data-ui-core";
|
||||
import {Observable} from 'rxjs';
|
||||
import {MatDialog, MatDialogConfig} from "@angular/material/dialog";
|
||||
import {
|
||||
@ -17,7 +17,7 @@ export interface IMDCommandDirectiveServiceOptions extends IConfirmOptions {
|
||||
export class CommandDirectiveService implements ICommandDirectiveService<IMDCommandDirectiveServiceOptions> {
|
||||
readonly dialog = inject(MatDialog);
|
||||
|
||||
confirm(options: IMDCommandDirectiveServiceOptions & IConfirmEvents): Observable<boolean> {
|
||||
confirm(options: ConfirmOptions<IMDCommandDirectiveServiceOptions>): Observable<boolean> {
|
||||
const defaultOptions: Partial<IMDCommandDirectiveServiceOptions> = {
|
||||
confirmText: 'Confirm',
|
||||
cancelText: 'Cancel'
|
||||
@ -39,7 +39,9 @@ export class CommandDirectiveService implements ICommandDirectiveService<IMDComm
|
||||
cancelText: finalOptions.cancelText,
|
||||
success: options.success,
|
||||
loading: options.loading,
|
||||
failure: options.failure
|
||||
failure: options.failure,
|
||||
formTemplate: options.formTemplate,
|
||||
form: options.form
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,11 @@
|
||||
<h2 mat-dialog-title>{{ title }}</h2>
|
||||
<mat-dialog-content>
|
||||
{{ message }}
|
||||
|
||||
@if (formTemplate) {
|
||||
<!-- [ngTemplateOutletContext]="{ $implicit: model, loading: loading, dataSource: dataSource }" -->
|
||||
<ng-container [ngTemplateOutlet]="formTemplate"></ng-container>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close cdkFocusInitial [disabled]="isLoading">{{ cancelText ?? 'Cancel' }}</button>
|
||||
|
@ -1,17 +1,20 @@
|
||||
import {Component, EventEmitter, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Component, EventEmitter, inject, OnDestroy, OnInit, TemplateRef} from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent, MatDialogRef,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {AsyncPipe, NgTemplateOutlet} from "@angular/common";
|
||||
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
||||
import {Observable, Subscription} from "rxjs";
|
||||
import {FormGroup} from "@angular/forms";
|
||||
import {FormType} from "@openharbor/ngx-data-ui-core";
|
||||
|
||||
export interface IConfirmDialogDefaultOptions {
|
||||
export interface IConfirmDialogDefaultOptions<TControl extends FormType<TControl> = any> {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
@ -19,10 +22,11 @@ export interface IConfirmDialogDefaultOptions {
|
||||
success: Observable<any>;
|
||||
failure: Observable<any>;
|
||||
loading: Observable<boolean>;
|
||||
formTemplate?: TemplateRef<any>;
|
||||
form?: FormGroup<TControl>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mdx-confirm-dialog-default',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatDialogContent,
|
||||
@ -31,13 +35,14 @@ export interface IConfirmDialogDefaultOptions {
|
||||
MatDialogClose,
|
||||
MatDialogTitle,
|
||||
AsyncPipe,
|
||||
MatProgressSpinner
|
||||
MatProgressSpinner,
|
||||
NgTemplateOutlet
|
||||
],
|
||||
templateUrl: './confirm-dialog-default.component.html',
|
||||
styleUrl: './confirm-dialog-default.component.css'
|
||||
})
|
||||
export class ConfirmDialogDefaultComponent implements OnInit, OnDestroy {
|
||||
readonly data = inject<IConfirmDialogDefaultOptions>(MAT_DIALOG_DATA);
|
||||
export class ConfirmDialogDefaultComponent<TControl extends FormType<TControl> = any> implements OnInit, OnDestroy {
|
||||
readonly data = inject<IConfirmDialogDefaultOptions<TControl>>(MAT_DIALOG_DATA);
|
||||
readonly ref = inject(MatDialogRef<ConfirmDialogDefaultComponent>);
|
||||
|
||||
readonly title: string = this.data.title;
|
||||
@ -46,6 +51,9 @@ export class ConfirmDialogDefaultComponent implements OnInit, OnDestroy {
|
||||
readonly cancelText?: string = this.data.cancelText;
|
||||
readonly $loading: Observable<boolean> = this.data.loading;
|
||||
readonly success: Observable<any> = this.data.success;
|
||||
readonly formTemplate?: TemplateRef<any> = this.data.formTemplate;
|
||||
readonly form?: FormGroup<TControl>;
|
||||
|
||||
isLoading = false;
|
||||
// todo: error messaging?
|
||||
|
||||
@ -75,6 +83,18 @@ export class ConfirmDialogDefaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onConfirmed() {
|
||||
if (!this.formTemplate || !this.form) {
|
||||
this._onConfirm.emit();
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.clearValidators();
|
||||
|
||||
if (!this.form.valid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this._onConfirm.emit();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
<div class="mat-elevation-z8">
|
||||
@if ($data) {
|
||||
<table mat-table [dataSource]="$data" matSort
|
||||
(matSortChange)="onSortChanged($event)">
|
||||
@for (column of _options.columns; track column.column) {
|
||||
<ng-container [matColumnDef]="column.column">
|
||||
<th mat-header-cell *matHeaderCellDef
|
||||
[mat-sort-header]="column.sorting && column.sorting.path ? column.sorting.path : column.column"
|
||||
[disabled]="!(column.sorting?.enable)"
|
||||
[sortActionDescription]="column.sorting && column.sorting.description ? (column.sorting.description(column.column) | async) ?? '' : 'Sort by ' + column.column">
|
||||
@if (column.title) {
|
||||
{{ column.title(column.column) | async }}
|
||||
}
|
||||
@else {
|
||||
{{ column.column | titlecase }}
|
||||
}
|
||||
</th>
|
||||
|
||||
<td mat-cell *matCellDef="let element">
|
||||
@if (column.value) {
|
||||
{{ column.value(element[column.column]) | async }}
|
||||
}
|
||||
@else {
|
||||
{{ element[column.column] }}
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="getColumns()"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: getColumns();"></tr>
|
||||
</table>
|
||||
|
||||
@if(_options.pagination.enable) {
|
||||
<mat-paginator
|
||||
[disabled]="isLoading"
|
||||
[hidePageSize]="_options.pagination.hidePageSize"
|
||||
[pageSizeOptions]="_options.pagination.pageSizeOptions"
|
||||
(page)="onPaginationEvent($event)"
|
||||
[length]="totalRecords"
|
||||
showFirstLastButtons
|
||||
aria-label="Select page of periodic elements">
|
||||
</mat-paginator>
|
||||
}
|
||||
|
||||
@if (isLoading) {
|
||||
<mat-progress-bar mode="query"></mat-progress-bar>
|
||||
}
|
||||
}
|
||||
</div>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DataGridComponent } from './data-grid.component';
|
||||
|
||||
describe('DataGridComponent', () => {
|
||||
let component: DataGridComponent;
|
||||
let fixture: ComponentFixture<DataGridComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DataGridComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DataGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,161 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {IDataSource} from "@openharbor/data";
|
||||
import {
|
||||
MatCell,
|
||||
MatCellDef,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderRow,
|
||||
MatHeaderRowDef,
|
||||
MatRow,
|
||||
MatRowDef,
|
||||
MatTable
|
||||
} from "@angular/material/table";
|
||||
import {MatPaginator, PageEvent} from "@angular/material/paginator";
|
||||
import {map, Observable, Subscription, tap} from "rxjs";
|
||||
import {MatProgressBar} from "@angular/material/progress-bar";
|
||||
import {AsyncPipe, JsonPipe, TitleCasePipe} from "@angular/common";
|
||||
import {MatSort, MatSortHeader, Sort} from "@angular/material/sort";
|
||||
|
||||
export interface IDataGridOptions<TModel extends object> {
|
||||
columns: IColumnDefinition<TModel, Extract<keyof TModel, string>>[];
|
||||
actions?: [];
|
||||
pagination: {
|
||||
enable: boolean;
|
||||
pageSize: number;
|
||||
hidePageSize: boolean;
|
||||
pageSizeOptions: number[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface IColumnDefinition<TModel extends object, TColumn extends Extract<keyof TModel, string>> {
|
||||
column: TColumn;
|
||||
sorting?: {
|
||||
enable: boolean;
|
||||
description?: (element: TColumn) => Observable<string>;
|
||||
path?: string;
|
||||
},
|
||||
title?: (element: TColumn) => Observable<string>;
|
||||
value?: (element: TModel[TColumn]) => Observable<string|number|boolean>;
|
||||
}
|
||||
|
||||
export interface IRowAction<TModel extends object> {
|
||||
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mdx-data-grid',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatCell,
|
||||
MatPaginator,
|
||||
MatHeaderRow,
|
||||
MatHeaderRowDef,
|
||||
MatRow,
|
||||
MatRowDef,
|
||||
MatProgressBar,
|
||||
MatHeaderCellDef,
|
||||
MatCellDef,
|
||||
TitleCasePipe,
|
||||
JsonPipe,
|
||||
AsyncPipe,
|
||||
MatSort,
|
||||
MatSortHeader
|
||||
],
|
||||
templateUrl: './data-grid.component.html',
|
||||
styleUrl: './data-grid.component.css'
|
||||
})
|
||||
export class DataGridComponent<TModel extends object> implements OnInit, OnDestroy {
|
||||
@Input({ required: true }) dataSource!: IDataSource<TModel>;
|
||||
@Input() options?: Partial<IDataGridOptions<TModel>>;
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
$data?: Observable<TModel[]>;
|
||||
totalRecords: number = 0;
|
||||
isLoading: boolean = false;
|
||||
|
||||
_options: IDataGridOptions<TModel> = {
|
||||
columns: [],
|
||||
pagination: {
|
||||
enable: true,
|
||||
hidePageSize: false,
|
||||
pageSizeOptions: [10, 25, 50, 100],
|
||||
pageSize: 25
|
||||
}
|
||||
};
|
||||
|
||||
getColumns() {
|
||||
return this._options.columns.map(options => {
|
||||
return options.column;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._options = {
|
||||
...this._options,
|
||||
...this.options
|
||||
};
|
||||
|
||||
this.$data = this.dataSource.data$
|
||||
.pipe(
|
||||
tap((result => {
|
||||
this.totalRecords = result?.totalRecords ?? 0;
|
||||
})),
|
||||
map((result) => {
|
||||
return result?.data ?? [];
|
||||
})
|
||||
);
|
||||
|
||||
this.subscriptions.push(this.dataSource.loading$
|
||||
.subscribe(loading => this.isLoading = loading)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
for (let subscription of this.subscriptions)
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
|
||||
onPaginationEvent(ev: PageEvent) {
|
||||
this.setPageSize(ev.pageSize);
|
||||
this.setPage(ev.pageIndex);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
setPage(index: number) {
|
||||
this.dataSource.page = index + 1;
|
||||
}
|
||||
|
||||
setPageSize(size: number) {
|
||||
this._options.pagination.pageSize = size;
|
||||
this.dataSource.pageSize = size;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.dataSource.refresh();
|
||||
}
|
||||
|
||||
onSortChanged(state: Sort) {
|
||||
if (state.direction === '') {
|
||||
this.dataSource.sorts = this.dataSource.sorts.filter(sort => sort.path !== state.active);
|
||||
} else {
|
||||
let field = this.dataSource.sorts.find(field => field.path === state.active);
|
||||
if (field === undefined) {
|
||||
field = {
|
||||
path: state.active
|
||||
};
|
||||
this.dataSource.sorts.push(field);
|
||||
}
|
||||
|
||||
|
||||
field.ascending = state.direction === 'asc';
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
@ -4,3 +4,4 @@
|
||||
|
||||
export * from './lib/command/services/command-directive.service';
|
||||
export * from './lib/confirm-dialog/confirm-dialog-default/confirm-dialog-default.component';
|
||||
export * from './lib/data-grid/data-grid/data-grid.component';
|
||||
|
Loading…
Reference in New Issue
Block a user