Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support add and extend field #WIK-16038 #10

Merged
merged 3 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -14,22 +13,22 @@ export abstract class AbstractEditCellEditor<TValue, TFieldType extends AITableF

record = input.required<AITableRecord>();

aiTable = input.required<AITable>();
@Input({ required: true }) aiTable!: AITable;

modelValue!: TValue;

protected thyPopoverRef = inject(ThyPopoverRef<AbstractEditCellEditor<TValue>>);

ngOnInit(): void {
this.modelValue = computed(() => {
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() {
Expand Down
10 changes: 10 additions & 0 deletions packages/grid/src/components/field-menu/field-menu.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@for (menu of fieldMenus; track index; let index = $index) {
@if (menu.id === 'divider') {
<thy-divider [thyStyle]="'solid'"></thy-divider>
} @else {
<a thyDropdownMenuItem href="javascript:;" (click)="execute(menu)">
<thy-icon [thyIconName]="menu.icon!"></thy-icon>
<span>{{ menu.name! }}</span>
</a>
}
}
40 changes: 40 additions & 0 deletions packages/grid/src/components/field-menu/field-menu.component.ts
Original file line number Diff line number Diff line change
@@ -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<any>;

execute(menu: AITableFieldMenu) {
const field = signal({ ...this.field });
menu.exec && menu.exec(this.aiTable, field, this.origin);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
thyAutofocus
name="fieldName"
[maxlength]="fieldMaxLength"
[(ngModel)]="field().name"
[(ngModel)]="aiField().name"
required
placeholder="输入列名称"
[thyUniqueCheck]="checkUniqueName"
Expand All @@ -27,9 +27,15 @@
</thy-list-item>
</div>
</thy-form-group>
<ng-container *ngIf="aiExternalTemplate; else defaultTemplate">
<ng-container *ngTemplateOutlet="aiExternalTemplate"></ng-container>
</ng-container>
<ng-template #defaultTemplate>
<!-- TODO: 内部属性渲染 -->
</ng-template>
<thy-form-group-footer thyAlign="right">
<button thyButton="link-secondary" (click)="cancel()" thySize="sm">取消</button>
<button thyButton="primary" (thyFormSubmit)="addField()" thySize="sm">确定</button>
<button thyButton="primary" (thyFormSubmit)="editFieldProperty()" thySize="sm">确定</button>
</thy-form-group-footer>
</form>

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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'
Expand All @@ -54,15 +54,17 @@ import { ThyListItem } from 'ngx-tethys/list';
`
]
})
export class FieldPropertyEditorComponent implements OnInit {
fields = input.required<AITableFields>();
export class AITableFieldPropertyEditor {
aiField = model.required<AITableField>();

@Input({ required: true }) aiTable!: AITable;

@Input({ required: true }) confirmAction: ((field: AITableField) => void) | null = null;
@Input() aiExternalTemplate: TemplateRef<any> | null = null;

field: WritableSignal<AITableField> = 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;
Expand All @@ -76,25 +78,27 @@ export class FieldPropertyEditorComponent implements OnInit {
}
};

selectableFields = FieldTypes;
selectableFields = Fields;

protected thyPopoverRef = inject(ThyPopoverRef<FieldPropertyEditorComponent>);
protected thyPopoverRef = inject(ThyPopoverRef<AITableFieldPropertyEditor>);

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();
}

Expand Down
1 change: 1 addition & 0 deletions packages/grid/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './field-property-editor/field-property-editor.component'
20 changes: 20 additions & 0 deletions packages/grid/src/constants/field.ts
Original file line number Diff line number Diff line change
@@ -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<AITableField>, origin?: HTMLElement | ElementRef<any>) => {
const fieldService = AI_TABLE_GRID_FIELD_SERVICE_MAP.get(aiTable);
origin && fieldService?.editFieldProperty(origin, aiTable, field, true);
}
};

export const DefaultFieldMenus: AITableFieldMenu[] = [EditFieldPropertyItem];
2 changes: 1 addition & 1 deletion packages/grid/src/constants/grid.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AITableFieldType } from '../core';
import { AITable, AITableFieldType } from '../core';

export const DEFAULT_COLUMN_WIDTH = 200;

Expand Down
1 change: 1 addition & 0 deletions packages/grid/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './grid';
export * from './field';
4 changes: 2 additions & 2 deletions packages/grid/src/core/action/field.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
6 changes: 3 additions & 3 deletions packages/grid/src/core/action/record.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions packages/grid/src/core/constants/field.ts
Original file line number Diff line number Diff line change
@@ -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: '文本',
Expand Down Expand Up @@ -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');
1 change: 1 addition & 0 deletions packages/grid/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './types';
export * from './action';
export * from './utils';
export * from './constants/field';
14 changes: 8 additions & 6 deletions packages/grid/src/core/types/action.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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;
};

Expand Down
6 changes: 6 additions & 0 deletions packages/grid/src/core/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,9 @@ export interface AITableChangeOptions {
fields: AITableField[];
actions: AITableAction[];
}

export interface AITableFieldInfo {
Copy link
Contributor

@Maple13 Maple13 Jul 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感觉这个 AITableFieldInfo 很模糊,它实际是列类型的菜单定义对吧,如果实在没有更好的命名,这样也行

type: AITableFieldType;
name: string;
icon: string;
}
14 changes: 13 additions & 1 deletion packages/grid/src/core/utils/field.ts
Original file line number Diff line number Diff line change
@@ -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) };
}
10 changes: 5 additions & 5 deletions packages/grid/src/core/utils/queries.ts
Original file line number Diff line number Diff line change
@@ -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}]`);
}
Expand Down
Loading