From 74edbbbca3f387454bb22f436eaef0f67367b5e1 Mon Sep 17 00:00:00 2001 From: huanhuanwa <44698191+huanhuanwa@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:55:51 +0800 Subject: [PATCH] feat: support add and extend field #WIK-16038 (#10) --- .../abstract-cell-editor.component.ts | 15 ++--- .../field-menu/field-menu.component.html | 10 +++ .../field-menu/field-menu.component.ts | 40 +++++++++++ .../field-property-editor.component.html | 10 ++- .../field-property-editor.component.ts | 44 ++++++------ packages/grid/src/components/index.ts | 1 + packages/grid/src/constants/field.ts | 20 ++++++ packages/grid/src/constants/grid.ts | 2 +- packages/grid/src/constants/index.ts | 1 + packages/grid/src/core/action/field.ts | 4 +- packages/grid/src/core/action/record.ts | 6 +- packages/grid/src/core/constants/field.ts | 8 +-- packages/grid/src/core/index.ts | 1 + packages/grid/src/core/types/action.ts | 14 ++-- packages/grid/src/core/types/core.ts | 6 ++ packages/grid/src/core/utils/field.ts | 14 +++- packages/grid/src/core/utils/queries.ts | 10 +-- packages/grid/src/grid.component.html | 13 +++- packages/grid/src/grid.component.ts | 67 ++++++++++++------- packages/grid/src/public-api.ts | 3 +- packages/grid/src/services/event.service.ts | 6 +- packages/grid/src/services/field.service.ts | 32 +++++++++ packages/grid/src/types/field.ts | 11 +++ packages/grid/src/types/grid.ts | 13 +++- packages/grid/src/types/index.ts | 1 + packages/grid/src/utils/global.ts | 5 -- src/app/app.component.html | 10 ++- src/app/app.component.scss | 3 + src/app/app.component.ts | 49 ++++++++++++-- .../field-property-editor.component.html | 8 +++ .../field-property-editor.component.ts | 21 ++++++ 31 files changed, 349 insertions(+), 99 deletions(-) create mode 100644 packages/grid/src/components/field-menu/field-menu.component.html create mode 100644 packages/grid/src/components/field-menu/field-menu.component.ts create mode 100644 packages/grid/src/components/index.ts create mode 100644 packages/grid/src/constants/field.ts create mode 100644 packages/grid/src/services/field.service.ts create mode 100644 packages/grid/src/types/field.ts delete mode 100644 packages/grid/src/utils/global.ts create mode 100644 src/app/component/field-property-editor/field-property-editor.component.html create mode 100644 src/app/component/field-property-editor/field-property-editor.component.ts diff --git a/packages/grid/src/components/cell-editors/abstract-cell-editor.component.ts b/packages/grid/src/components/cell-editors/abstract-cell-editor.component.ts index 59701ba2..6803ad72 100644 --- a/packages/grid/src/components/cell-editors/abstract-cell-editor.component.ts +++ b/packages/grid/src/components/cell-editors/abstract-cell-editor.component.ts @@ -1,7 +1,6 @@ -import { ChangeDetectionStrategy, Component, computed, inject, input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, Input, input, OnInit } from '@angular/core'; import { ThyPopoverRef } from 'ngx-tethys/popover'; -import { GridCellPath } from '../../types'; -import { Actions, AITable, AITableField, AITableQueries, AITableRecord } from '../../core'; +import { Actions, AIFieldValuePath, AITable, AITableField, AITableQueries, AITableRecord } from '../../core'; @Component({ selector: 'abstract-edit-cell', @@ -14,7 +13,7 @@ export abstract class AbstractEditCellEditor(); - aiTable = input.required(); + @Input({ required: true }) aiTable!: AITable; modelValue!: TValue; @@ -22,14 +21,14 @@ export abstract class AbstractEditCellEditor { - const path = AITableQueries.findPath(this.aiTable(), this.field(), this.record()) as GridCellPath; - return AITableQueries.getFieldValue(this.aiTable(), path); + const path = AITableQueries.findPath(this.aiTable, this.field(), this.record()) as AIFieldValuePath; + return AITableQueries.getFieldValue(this.aiTable, path); })(); } updateFieldValue() { - const path = AITableQueries.findPath(this.aiTable(), this.field(), this.record()) as GridCellPath; - Actions.updateFieldValue(this.aiTable(), this.modelValue, path); + const path = AITableQueries.findPath(this.aiTable, this.field(), this.record()) as AIFieldValuePath; + Actions.updateFieldValue(this.aiTable, this.modelValue, path); } closePopover() { diff --git a/packages/grid/src/components/field-menu/field-menu.component.html b/packages/grid/src/components/field-menu/field-menu.component.html new file mode 100644 index 00000000..eb11e089 --- /dev/null +++ b/packages/grid/src/components/field-menu/field-menu.component.html @@ -0,0 +1,10 @@ +@for (menu of fieldMenus; track index; let index = $index) { + @if (menu.id === 'divider') { + + } @else { + + + {{ menu.name! }} + + } +} diff --git a/packages/grid/src/components/field-menu/field-menu.component.ts b/packages/grid/src/components/field-menu/field-menu.component.ts new file mode 100644 index 00000000..fd49f9de --- /dev/null +++ b/packages/grid/src/components/field-menu/field-menu.component.ts @@ -0,0 +1,40 @@ +import { Component, ChangeDetectionStrategy, Input, ElementRef, signal } from '@angular/core'; +import { AITableFieldMenu } from '../../types/field'; +import { AITableField, AITable } from '../../core'; +import { + ThyDropdownMenuItemDirective, + ThyDropdownMenuItemNameDirective, + ThyDropdownMenuItemIconDirective, + ThyDropdownMenuComponent +} from 'ngx-tethys/dropdown'; +import { ThyIcon } from 'ngx-tethys/icon'; +import { ThyDivider } from 'ngx-tethys/divider'; + +@Component({ + selector: 'field-menu', + templateUrl: './field-menu.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ThyIcon, + ThyDivider, + ThyDropdownMenuComponent, + ThyDropdownMenuItemDirective, + ThyDropdownMenuItemNameDirective, + ThyDropdownMenuItemIconDirective + ] +}) +export class FieldMenu { + @Input({ required: true }) field!: AITableField; + + @Input({ required: true }) aiTable!: AITable; + + @Input({ required: true }) fieldMenus!: AITableFieldMenu[]; + + @Input() origin!: HTMLElement | ElementRef; + + execute(menu: AITableFieldMenu) { + const field = signal({ ...this.field }); + menu.exec && menu.exec(this.aiTable, field, this.origin); + } +} diff --git a/packages/grid/src/components/field-property-editor/field-property-editor.component.html b/packages/grid/src/components/field-property-editor/field-property-editor.component.html index 3a225ca7..aa8370fb 100644 --- a/packages/grid/src/components/field-property-editor/field-property-editor.component.html +++ b/packages/grid/src/components/field-property-editor/field-property-editor.component.html @@ -6,7 +6,7 @@ thyAutofocus name="fieldName" [maxlength]="fieldMaxLength" - [(ngModel)]="field().name" + [(ngModel)]="aiField().name" required placeholder="输入列名称" [thyUniqueCheck]="checkUniqueName" @@ -27,9 +27,15 @@ + + + + + + - + diff --git a/packages/grid/src/components/field-property-editor/field-property-editor.component.ts b/packages/grid/src/components/field-property-editor/field-property-editor.component.ts index c87f54c7..e8f3e727 100644 --- a/packages/grid/src/components/field-property-editor/field-property-editor.component.ts +++ b/packages/grid/src/components/field-property-editor/field-property-editor.component.ts @@ -1,5 +1,5 @@ -import { NgForOf, NgIf } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input, OnInit, WritableSignal, computed, inject, input, signal } from '@angular/core'; +import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input, TemplateRef, booleanAttribute, computed, inject, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ThyInput, ThyInputCount, ThyInputGroup, ThyInputDirective } from 'ngx-tethys/input'; import { ThyConfirmValidatorDirective, ThyUniqueCheckValidator, ThyFormValidatorConfig, ThyFormModule } from 'ngx-tethys/form'; @@ -11,15 +11,14 @@ import { ThyDropdownMenuItemIconDirective } from 'ngx-tethys/dropdown'; import { ThyButton } from 'ngx-tethys/button'; -import { of } from 'rxjs'; -import { AITableField, AITableFieldType, AITableFields, idCreator } from '../../core'; +import { AITable, AITableField, AITableFieldType, Actions, Fields, FieldsMap, createDefaultFieldName } from '../../core'; import { ThyIcon } from 'ngx-tethys/icon'; -import { FieldTypes, FieldTypesMap } from '../../core/constants/field'; import { ThyPopoverRef } from 'ngx-tethys/popover'; import { ThyListItem } from 'ngx-tethys/list'; +import { of } from 'rxjs'; @Component({ - selector: 'field-property-editor', + selector: 'ai-table-field-property-editor', templateUrl: './field-property-editor.component.html', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, @@ -41,7 +40,8 @@ import { ThyListItem } from 'ngx-tethys/list'; ThyDropdownMenuItemIconDirective, ThyButton, ThyFormModule, - ThyListItem + ThyListItem, + NgTemplateOutlet ], host: { class: 'field-property-editor d-block pl-5 pr-5 pb-5 pt-4' @@ -54,15 +54,17 @@ import { ThyListItem } from 'ngx-tethys/list'; ` ] }) -export class FieldPropertyEditorComponent implements OnInit { - fields = input.required(); +export class AITableFieldPropertyEditor { + aiField = model.required(); + + @Input({ required: true }) aiTable!: AITable; - @Input({ required: true }) confirmAction: ((field: AITableField) => void) | null = null; + @Input() aiExternalTemplate: TemplateRef | null = null; - field: WritableSignal = signal({ id: idCreator(), type: AITableFieldType.Text, name: '' }); + @Input({ transform: booleanAttribute }) isUpdate!: boolean; fieldType = computed(() => { - return FieldTypesMap[this.field().type]; + return FieldsMap[this.aiField().type]; }); fieldMaxLength = 32; @@ -76,25 +78,27 @@ export class FieldPropertyEditorComponent implements OnInit { } }; - selectableFields = FieldTypes; + selectableFields = Fields; - protected thyPopoverRef = inject(ThyPopoverRef); + protected thyPopoverRef = inject(ThyPopoverRef); constructor() {} - ngOnInit() {} - checkUniqueName = (fieldName: string) => { fieldName = fieldName?.trim(); - return of(!!this.fields()?.find((field) => field.name === fieldName)); + return of(!!this.aiTable.fields()?.find((field) => field.name === fieldName && this.aiField()?.id !== field.id)); }; selectFieldType(fieldType: AITableFieldType) { - this.field.update((item) => ({ ...item, type: fieldType })); + this.aiField.update((item) => ({ ...item, type: fieldType, name: createDefaultFieldName(this.aiTable, fieldType) })); } - addField() { - this.confirmAction!(this.field()); + editFieldProperty() { + if (this.isUpdate) { + //TODO: updateField + } else { + Actions.addField(this.aiTable, this.aiField(), [this.aiTable.fields().length]); + } this.thyPopoverRef.close(); } diff --git a/packages/grid/src/components/index.ts b/packages/grid/src/components/index.ts new file mode 100644 index 00000000..3cba1bcf --- /dev/null +++ b/packages/grid/src/components/index.ts @@ -0,0 +1 @@ +export * from './field-property-editor/field-property-editor.component' \ No newline at end of file diff --git a/packages/grid/src/constants/field.ts b/packages/grid/src/constants/field.ts new file mode 100644 index 00000000..44d9cec6 --- /dev/null +++ b/packages/grid/src/constants/field.ts @@ -0,0 +1,20 @@ +import { AITable, AITableField } from '../core'; +import { AITableFieldMenu } from '../types/field'; +import { AI_TABLE_GRID_FIELD_SERVICE_MAP } from '../services/field.service'; +import { ElementRef, WritableSignal } from '@angular/core'; + +export const DividerMenuItem = { + id: 'divider' +}; + +export const EditFieldPropertyItem = { + id: 'editFieldProperty', + name: '编辑列', + icon: 'edit', + exec: (aiTable: AITable, field: WritableSignal, origin?: HTMLElement | ElementRef) => { + const fieldService = AI_TABLE_GRID_FIELD_SERVICE_MAP.get(aiTable); + origin && fieldService?.editFieldProperty(origin, aiTable, field, true); + } +}; + +export const DefaultFieldMenus: AITableFieldMenu[] = [EditFieldPropertyItem]; diff --git a/packages/grid/src/constants/grid.ts b/packages/grid/src/constants/grid.ts index daccb120..a91945cf 100644 --- a/packages/grid/src/constants/grid.ts +++ b/packages/grid/src/constants/grid.ts @@ -1,4 +1,4 @@ -import { AITableFieldType } from '../core'; +import { AITable, AITableFieldType } from '../core'; export const DEFAULT_COLUMN_WIDTH = 200; diff --git a/packages/grid/src/constants/index.ts b/packages/grid/src/constants/index.ts index d24d1bdc..ffc43be1 100644 --- a/packages/grid/src/constants/index.ts +++ b/packages/grid/src/constants/index.ts @@ -1 +1,2 @@ export * from './grid'; +export * from './field'; \ No newline at end of file diff --git a/packages/grid/src/core/action/field.ts b/packages/grid/src/core/action/field.ts index b421fa6a..83e9b66d 100644 --- a/packages/grid/src/core/action/field.ts +++ b/packages/grid/src/core/action/field.ts @@ -1,6 +1,6 @@ -import { ActionName, AddFieldAction, FieldPath, AITable, AITableField } from '../types'; +import { ActionName, AddFieldAction, AIFieldPath, AITable, AITableField } from '../types'; -export function addField(aiTable: AITable, field: AITableField, path: [FieldPath]) { +export function addField(aiTable: AITable, field: AITableField, path: AIFieldPath) { const operation: AddFieldAction = { type: ActionName.AddField, field, diff --git a/packages/grid/src/core/action/record.ts b/packages/grid/src/core/action/record.ts index de02a9ec..3d79a7cd 100644 --- a/packages/grid/src/core/action/record.ts +++ b/packages/grid/src/core/action/record.ts @@ -1,7 +1,7 @@ -import { ActionName, AddRecordAction, FieldPath, RecordPath, UpdateFieldValueAction, AITable, AITableRecord } from '../types'; +import { ActionName, AddRecordAction, AIRecordPath, UpdateFieldValueAction, AITable, AITableRecord, AIFieldValuePath } from '../types'; import { AITableQueries } from '../utils'; -export function updateFieldValue(aiTable: AITable, value: any, path: [RecordPath, FieldPath]) { +export function updateFieldValue(aiTable: AITable, value: any, path: AIFieldValuePath) { const node = AITableQueries.getFieldValue(aiTable, path); if (node !== value) { const operation: UpdateFieldValueAction = { @@ -14,7 +14,7 @@ export function updateFieldValue(aiTable: AITable, value: any, path: [RecordPath } } -export function addRecord(aiTable: AITable, record: AITableRecord, path: [RecordPath]) { +export function addRecord(aiTable: AITable, record: AITableRecord, path: AIRecordPath) { const operation: AddRecordAction = { type: ActionName.AddRecord, record, diff --git a/packages/grid/src/core/constants/field.ts b/packages/grid/src/core/constants/field.ts index f1f3a532..8206b6aa 100644 --- a/packages/grid/src/core/constants/field.ts +++ b/packages/grid/src/core/constants/field.ts @@ -1,7 +1,7 @@ -import { AITableFieldType } from '../types'; +import { AITableFieldInfo, AITableFieldType } from '../types'; import { helpers } from 'ngx-tethys/util'; -export const BasicFieldTypes = [ +export const BasicFields = [ { type: AITableFieldType.Text, name: '文本', @@ -34,6 +34,6 @@ export const BasicFieldTypes = [ } ]; -export const FieldTypes = [...BasicFieldTypes]; +export const Fields: AITableFieldInfo[] = [...BasicFields]; -export const FieldTypesMap = helpers.keyBy([...FieldTypes], 'type'); +export const FieldsMap = helpers.keyBy([...BasicFields], 'type'); diff --git a/packages/grid/src/core/index.ts b/packages/grid/src/core/index.ts index 260fe641..b95c7988 100644 --- a/packages/grid/src/core/index.ts +++ b/packages/grid/src/core/index.ts @@ -1,3 +1,4 @@ export * from './types'; export * from './action'; export * from './utils'; +export * from './constants/field'; \ No newline at end of file diff --git a/packages/grid/src/core/types/action.ts b/packages/grid/src/core/types/action.ts index 604ddbe3..0444151a 100644 --- a/packages/grid/src/core/types/action.ts +++ b/packages/grid/src/core/types/action.ts @@ -1,10 +1,12 @@ import { AITableField, AITableRecord } from './core'; -export type RecordPath = number; +export type AIRecordPath = [number]; -export type FieldPath = number; +export type AIFieldPath = [number]; -export type Path = [RecordPath] | [FieldPath] | [RecordPath, FieldPath]; +export type AIFieldValuePath = [number, number]; + +export type Path = AIRecordPath | AIFieldPath | AIFieldValuePath; export enum ActionName { UpdateFieldValue = 'update_field_value', @@ -20,20 +22,20 @@ export enum ExecuteType { export type UpdateFieldValueAction = { type: ActionName.UpdateFieldValue; - path: [RecordPath, FieldPath]; + path: AIFieldValuePath; fieldValue: any; newFieldValue: any; }; export type AddRecordAction = { type: ActionName.AddRecord; - path: [RecordPath]; + path: AIRecordPath; record: AITableRecord; }; export type AddFieldAction = { type: ActionName.AddField; - path: [FieldPath]; + path: AIFieldPath; field: AITableField; }; diff --git a/packages/grid/src/core/types/core.ts b/packages/grid/src/core/types/core.ts index 3291573d..efb2eff9 100644 --- a/packages/grid/src/core/types/core.ts +++ b/packages/grid/src/core/types/core.ts @@ -82,3 +82,9 @@ export interface AITableChangeOptions { fields: AITableField[]; actions: AITableAction[]; } + +export interface AITableFieldInfo { + type: AITableFieldType; + name: string; + icon: string; +} diff --git a/packages/grid/src/core/utils/field.ts b/packages/grid/src/core/utils/field.ts index b2d20340..2855860f 100644 --- a/packages/grid/src/core/utils/field.ts +++ b/packages/grid/src/core/utils/field.ts @@ -1,5 +1,17 @@ -import { AITableFieldType } from '../types'; +import { FieldsMap } from '../constants/field'; +import { AITable, AITableFieldType } from '../types'; +import { idCreator } from './id-creator'; export function getDefaultFieldValue(type: AITableFieldType) { return ''; } + +export function createDefaultFieldName(aiTable: AITable, type: AITableFieldType = AITableFieldType.Text) { + const fields = aiTable.fields(); + const count = fields.filter((item) => item.type === type).length; + return count === 0 ? FieldsMap[type].name : FieldsMap[type].name + count; +} + +export function createDefaultField(aiTable: AITable, type: AITableFieldType = AITableFieldType.Text) { + return { id: idCreator(), type, name: createDefaultFieldName(aiTable, type) }; +} diff --git a/packages/grid/src/core/utils/queries.ts b/packages/grid/src/core/utils/queries.ts index 842c7c6f..83792fea 100644 --- a/packages/grid/src/core/utils/queries.ts +++ b/packages/grid/src/core/utils/queries.ts @@ -1,22 +1,22 @@ import { isUndefinedOrNull } from 'ngx-tethys/util'; -import { FieldPath, Path, RecordPath, AITable, AITableField, AITableRecord } from '../types'; +import { Path, AITable, AITableField, AITableRecord, AIFieldValuePath, AIRecordPath, AIFieldPath } from '../types'; export const AITableQueries = { findPath(aiTable: AITable, field?: AITableField, record?: AITableRecord): Path { const recordIndex = record && aiTable.records().indexOf(record); const fieldIndex = field && aiTable.fields().indexOf(field); if (!isUndefinedOrNull(recordIndex) && recordIndex > -1 && !isUndefinedOrNull(fieldIndex) && fieldIndex > -1) { - return [recordIndex!, fieldIndex!]; + return [recordIndex!, fieldIndex!] as AIFieldValuePath; } if (!isUndefinedOrNull(recordIndex) && recordIndex > -1) { - return [recordIndex]; + return [recordIndex] as AIRecordPath; } if (!isUndefinedOrNull(fieldIndex) && fieldIndex > -1) { - return [fieldIndex]; + return [fieldIndex] as AIFieldPath; } throw new Error(`Unable to find the path: ${JSON.stringify({ ...(field || {}), ...(record || {}) })}`); }, - getFieldValue(aiTable: AITable, path: [RecordPath, FieldPath]): any { + getFieldValue(aiTable: AITable, path: [number, number]): any { if (!aiTable || !aiTable.records() || !aiTable.fields()) { throw new Error(`Cannot find a descendant at path [${path}]`); } diff --git a/packages/grid/src/grid.component.html b/packages/grid/src/grid.component.html index 8cb46842..6dacd21f 100644 --- a/packages/grid/src/grid.component.html +++ b/packages/grid/src/grid.component.html @@ -3,9 +3,18 @@ @for (field of gridData().fields; track field.id) { -
{{ field.name }}
+
+ {{ field.name }} + + + + + +
} -
+
+ +
@for (record of gridData().records; track record.id; let index = $index) { diff --git a/packages/grid/src/grid.component.ts b/packages/grid/src/grid.component.ts index 670249e7..ea6665e7 100644 --- a/packages/grid/src/grid.component.ts +++ b/packages/grid/src/grid.component.ts @@ -1,32 +1,36 @@ -import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, input, model, OnInit, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, ElementRef, input, model, OnInit, output, signal, viewChild } from '@angular/core'; import { CommonModule, NgClass, NgComponentOutlet, NgForOf } from '@angular/common'; import { SelectOptionPipe } from './pipes/grid'; import { ThyTag } from 'ngx-tethys/tag'; -import { ThyPopover, ThyPopoverModule } from 'ngx-tethys/popover'; +import { ThyPopoverModule } from 'ngx-tethys/popover'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { buildGridData } from './utils'; -import { AITableGridCellRenderSchema, AITableRowHeight } from './types'; +import { AIFieldConfig, AITableFieldMenu, AITableRowHeight } from './types'; import { Actions, createAITable, getDefaultRecord, - idCreator, AITable, AITableChangeOptions, AITableFields, AITableFieldType, AITableRecords, - AITableField + createDefaultField } from './core'; import { ThyIcon } from 'ngx-tethys/icon'; import { AITableGridEventService } from './services/event.service'; -import { FieldPropertyEditorComponent } from './components/field-property-editor/field-property-editor.component'; +import { AITableFieldPropertyEditor } from './components/field-property-editor/field-property-editor.component'; import { ThyDatePickerFormatPipe } from 'ngx-tethys/date-picker'; import { ThyRate } from 'ngx-tethys/rate'; import { FormsModule } from '@angular/forms'; import { ThyFlexibleText } from 'ngx-tethys/flexible-text'; import { ThyTooltipModule, ThyTooltipService } from 'ngx-tethys/tooltip'; import { ThyStopPropagationDirective } from 'ngx-tethys/shared'; +import { FieldMenu } from './components/field-menu/field-menu.component'; +import { ThyAction } from 'ngx-tethys/action'; +import { ThyDropdownDirective, ThyDropdownMenuComponent } from 'ngx-tethys/dropdown'; +import { DefaultFieldMenus } from './constants'; +import { AI_TABLE_GRID_FIELD_SERVICE_MAP, AITableGridFieldService } from './services/field.service'; @Component({ selector: 'ai-table-grid', @@ -47,22 +51,26 @@ import { ThyStopPropagationDirective } from 'ngx-tethys/shared'; ThyPopoverModule, ThyIcon, ThyRate, - FieldPropertyEditorComponent, + AITableFieldPropertyEditor, ThyDatePickerFormatPipe, ThyTooltipModule, ThyFlexibleText, - ThyStopPropagationDirective + ThyStopPropagationDirective, + FieldMenu, + ThyAction, + ThyDropdownDirective, + ThyDropdownMenuComponent ], - providers: [ThyTooltipService, AITableGridEventService] + providers: [ThyTooltipService, AITableGridEventService, AITableGridFieldService] }) -export class AITableGridComponent implements OnInit { +export class AITableGrid implements OnInit { aiRecords = model.required(); aiFields = model.required(); aiRowHeight = input(); - aiFieldRenderers = input>>(); + aiFieldConfig = input(); aiReadonly = input(); @@ -74,6 +82,10 @@ export class AITableGridComponent implements OnInit { onChange = output(); + aiTableInitialized = output(); + + fieldMenus!: AITableFieldMenu[]; + gridData = computed(() => { return buildGridData(this.aiRecords(), this.aiFields()); }); @@ -81,17 +93,18 @@ export class AITableGridComponent implements OnInit { constructor( private elementRef: ElementRef, private aiTableGridEventService: AITableGridEventService, - private thyPopover: ThyPopover + private aiTableGridFieldService: AITableGridFieldService ) {} ngOnInit(): void { this.initAITable(); - this.aiTableGridEventService.initialize(this.aiTable, this.aiFieldRenderers()); - this.aiTableGridEventService.registerEvents(this.elementRef.nativeElement); + this.initService(); + this.buildFieldMenus(); } initAITable() { this.aiTable = createAITable(this.aiRecords, this.aiFields); + this.aiTableInitialized.emit(this.aiTable); this.aiTable.onChange = () => { this.onChange.emit({ records: this.aiRecords(), @@ -101,21 +114,23 @@ export class AITableGridComponent implements OnInit { }; } + initService() { + this.aiTableGridEventService.initialize(this.aiTable, this.aiFieldConfig()?.fieldPropertyEditor); + this.aiTableGridEventService.registerEvents(this.elementRef.nativeElement); + this.aiTableGridFieldService.initAIFieldConfig(this.aiFieldConfig()); + AI_TABLE_GRID_FIELD_SERVICE_MAP.set(this.aiTable, this.aiTableGridFieldService); + } + + buildFieldMenus() { + this.fieldMenus = this.aiFieldConfig()?.fieldMenus ?? DefaultFieldMenus; + } + addRecord() { Actions.addRecord(this.aiTable, getDefaultRecord(this.aiFields()), [this.aiRecords().length]); } - addField(event: Event) { - this.thyPopover.open(FieldPropertyEditorComponent, { - origin: event.currentTarget as HTMLElement, - manualClosure: true, - placement: 'bottomLeft', - initialState: { - fields: this.aiFields, - confirmAction: (field: AITableField) => { - Actions.addField(this.aiTable, field, [this.aiFields().length]); - } - } - }); + addField(gridColumnBlank: HTMLElement) { + const field = signal(createDefaultField(this.aiTable, AITableFieldType.Text)); + this.aiTableGridFieldService.editFieldProperty(gridColumnBlank, this.aiTable, field, false); } } diff --git a/packages/grid/src/public-api.ts b/packages/grid/src/public-api.ts index e8d83766..13853aee 100644 --- a/packages/grid/src/public-api.ts +++ b/packages/grid/src/public-api.ts @@ -3,4 +3,5 @@ export * from './types'; export * from './pipes'; export * from './constants'; export * from './utils'; -export * from './core'; \ No newline at end of file +export * from './core'; +export * from './components'; \ No newline at end of file diff --git a/packages/grid/src/services/event.service.ts b/packages/grid/src/services/event.service.ts index 79b49988..a9103ece 100644 --- a/packages/grid/src/services/event.service.ts +++ b/packages/grid/src/services/event.service.ts @@ -1,4 +1,4 @@ -import { Injectable, signal, Signal } from '@angular/core'; +import { Injectable, Signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { fromEvent } from 'rxjs'; import { DBL_CLICK_EDIT_TYPE } from '../constants'; @@ -41,7 +41,7 @@ export class AITableGridEventService { private getEditorComponent(type: AITableFieldType) { if (this.aiFieldRenderers && this.aiFieldRenderers[type]) { - return this.aiFieldRenderers[type]!.edit; + return this.aiFieldRenderers[type]!.editor; } return GRID_CELL_EDITOR_MAP[type]; } @@ -68,7 +68,7 @@ export class AITableGridEventService { initialState: { field: field, record: record, - aiTable: signal(this.aiTable) + aiTable: this.aiTable }, panelClass: 'grid-cell-editor', outsideClosable: false, diff --git a/packages/grid/src/services/field.service.ts b/packages/grid/src/services/field.service.ts new file mode 100644 index 00000000..59bc7c51 --- /dev/null +++ b/packages/grid/src/services/field.service.ts @@ -0,0 +1,32 @@ +import { ElementRef, Injectable, ModelSignal, WritableSignal } from '@angular/core'; +import { ThyPopover } from 'ngx-tethys/popover'; +import { AITable, AITableField, AITableFields } from '../core'; +import { AITableFieldPropertyEditor } from '../components'; +import { AIFieldConfig } from '../types'; + +export const AI_TABLE_GRID_FIELD_SERVICE_MAP = new WeakMap(); + +@Injectable() +export class AITableGridFieldService { + aiFieldConfig: AIFieldConfig | undefined; + + constructor(private thyPopover: ThyPopover) {} + + initAIFieldConfig(aiFieldConfig: AIFieldConfig | undefined) { + this.aiFieldConfig = aiFieldConfig; + } + + editFieldProperty(origin: HTMLElement | ElementRef, aiTable: AITable, aiField: WritableSignal, isUpdate: boolean) { + const component = this.aiFieldConfig?.fieldPropertyEditor ?? AITableFieldPropertyEditor; + this.thyPopover.open(component, { + origin: origin, + manualClosure: true, + placement: 'bottomLeft', + initialState: { + aiTable, + aiField, + isUpdate + } + }); + } +} diff --git a/packages/grid/src/types/field.ts b/packages/grid/src/types/field.ts new file mode 100644 index 00000000..c6881034 --- /dev/null +++ b/packages/grid/src/types/field.ts @@ -0,0 +1,11 @@ +import { ElementRef, Signal, WritableSignal } from '@angular/core'; +import { AITable, AITableField } from '../core'; + +export interface AITableFieldMenu { + id: string; + name?: string; + icon?: string; + exec?: (aiTable: AITable, field: WritableSignal, origin?: HTMLElement | ElementRef) => void; + hidden?: (aiTable: AITable, field: Signal) => boolean; + disabled?: (aiTable: AITable, field: Signal) => boolean; +} diff --git a/packages/grid/src/types/grid.ts b/packages/grid/src/types/grid.ts index 9c95af0a..735ce586 100644 --- a/packages/grid/src/types/grid.ts +++ b/packages/grid/src/types/grid.ts @@ -1,4 +1,5 @@ -import { FieldPath, RecordPath, AITableField, AITableRecord } from '../core'; +import { AITableField, AITableFieldType, AITableRecord } from '../core'; +import { AITableFieldMenu } from './field'; export enum AITableRowHeight { Short = 1, @@ -8,7 +9,7 @@ export enum AITableRowHeight { } export interface AITableGridCellRenderSchema { - edit: any; + editor: any; } export interface AITableGridData { @@ -17,4 +18,10 @@ export interface AITableGridData { records: AITableRecord[]; } -export type GridCellPath = [RecordPath, FieldPath]; +export interface AIFieldConfig { + fieldRenderers?: Partial>; + fieldPropertyEditor?: any; + fieldMenus?: AITableFieldMenu[]; +} + + diff --git a/packages/grid/src/types/index.ts b/packages/grid/src/types/index.ts index d24d1bdc..ffc43be1 100644 --- a/packages/grid/src/types/index.ts +++ b/packages/grid/src/types/index.ts @@ -1 +1,2 @@ export * from './grid'; +export * from './field'; \ No newline at end of file diff --git a/packages/grid/src/utils/global.ts b/packages/grid/src/utils/global.ts deleted file mode 100644 index 4663b394..00000000 --- a/packages/grid/src/utils/global.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Overlay } from '@angular/cdk/overlay'; - -export function thyPopoverScrollStrategyFactory(overlay: Overlay) { - return () => overlay.scrollStrategies.close(); -} diff --git a/src/app/app.component.html b/src/app/app.component.html index 7856ad06..db42c502 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1,9 @@ - + + + diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 5b971e99..fcd4b246 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,4 +1,7 @@ app-root { display: block; margin: 20px; + margin-bottom: 0; + overflow-x: auto; + height: calc(100vh - 20px); } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8dc0e358..f6875b72 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,8 +1,20 @@ -import { AfterViewInit, Component, OnInit, signal, WritableSignal } from '@angular/core'; +import { AfterViewInit, Component, OnInit, Signal, signal, WritableSignal } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { RouterOutlet } from '@angular/router'; -import { AITableFields, AITableFieldType, AITableGridComponent, AITableRecords } from '@ai-table/grid'; +import { + AITableFields, + AITableFieldType, + AITableGrid, + AITableRecords, + AITableField, + AITable, + AIFieldConfig, + EditFieldPropertyItem, + DividerMenuItem +} from '@ai-table/grid'; import { ThyIconRegistry } from 'ngx-tethys/icon'; +import { ThyPopover, ThyPopoverModule } from 'ngx-tethys/popover'; +import { FieldPropertyEditor } from './component/field-property-editor/field-property-editor.component'; const LOCAL_STORAGE_KEY = 'ai-table-data'; @@ -25,6 +37,7 @@ const initValue = { value: { 'column-1': '文本 2-1', 'column-2': '2', + 'column-3': {}, 'column-4': 1 } }, @@ -32,7 +45,9 @@ const initValue = { id: 'row-3', value: { 'column-1': '文本 3-1', - 'column-2': '3' + 'column-2': '3', + 'column-3': {}, + 'column-4': null } } ], @@ -102,7 +117,7 @@ const initValue = { @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, AITableGridComponent], + imports: [RouterOutlet, AITableGrid, ThyPopoverModule, FieldPropertyEditor], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) @@ -111,9 +126,28 @@ export class AppComponent implements OnInit, AfterViewInit { fields!: WritableSignal; + aiTable!: AITable; + + aiFieldConfig: AIFieldConfig = { + fieldPropertyEditor: FieldPropertyEditor, + fieldMenus: [ + EditFieldPropertyItem, + DividerMenuItem, + { + id: 'filterFields', + name: '按本列筛选', + icon: 'filter-line', + exec: (aiTable: AITable, field: Signal) => {}, + hidden: (aiTable: AITable, field: Signal) => false, + disabled: (aiTable: AITable, field: Signal) => false + } + ] + }; + constructor( private iconRegistry: ThyIconRegistry, - private sanitizer: DomSanitizer + private sanitizer: DomSanitizer, + private thyPopover: ThyPopover ) { this.registryIcon(); } @@ -127,7 +161,6 @@ export class AppComponent implements OnInit, AfterViewInit { registryIcon() { this.iconRegistry.addSvgIconSet(this.sanitizer.bypassSecurityTrustResourceUrl('assets/icons/defs/svg/sprite.defs.svg')); - this.iconRegistry.addSvgIconSet(this.sanitizer.bypassSecurityTrustResourceUrl('assets/icons/symbol/svg/sprite.defs.svg')); } ngAfterViewInit() { @@ -144,6 +177,10 @@ export class AppComponent implements OnInit, AfterViewInit { ); } + aiTableInitialized(aiTable: AITable) { + this.aiTable = aiTable; + } + setLocalData(data: string) { localStorage.setItem(`${LOCAL_STORAGE_KEY}`, data); } diff --git a/src/app/component/field-property-editor/field-property-editor.component.html b/src/app/component/field-property-editor/field-property-editor.component.html new file mode 100644 index 00000000..24958794 --- /dev/null +++ b/src/app/component/field-property-editor/field-property-editor.component.html @@ -0,0 +1,8 @@ + + + + + @if (aiField().type === AITableFieldType.SingleSelect) { + 下拉选项选择 + } + diff --git a/src/app/component/field-property-editor/field-property-editor.component.ts b/src/app/component/field-property-editor/field-property-editor.component.ts new file mode 100644 index 00000000..92891290 --- /dev/null +++ b/src/app/component/field-property-editor/field-property-editor.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, Input, WritableSignal, booleanAttribute, model } from '@angular/core'; +import { AITableField, AITableFieldType, AITableFieldPropertyEditor, AITable } from '@ai-table/grid'; + +@Component({ + selector: 'field-property-editor', + templateUrl: './field-property-editor.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AITableFieldPropertyEditor] +}) +export class FieldPropertyEditor { + aiField = model.required(); + + @Input({ required: true }) aiTable!: AITable; + + @Input({ transform: booleanAttribute }) isUpdate!: boolean; + + field!: WritableSignal; + + AITableFieldType = AITableFieldType; +}