diff --git a/packages/grid/src/constants/colors.ts b/packages/grid/src/constants/colors.ts index 92d1debf..945bcfdf 100644 --- a/packages/grid/src/constants/colors.ts +++ b/packages/grid/src/constants/colors.ts @@ -12,6 +12,7 @@ export const Colors = { gray80: '#fafafa', headSelectedBgColor: '#EAEFFA', itemActiveBgColor: '#6698ff1a', + itemMatchBgColor: '#ff9f731a', waring: '#ffcd5d', success: '#73d897' }; diff --git a/packages/grid/src/core/types/ai-table.ts b/packages/grid/src/core/types/ai-table.ts index 9bc4d09f..e0bac1f9 100644 --- a/packages/grid/src/core/types/ai-table.ts +++ b/packages/grid/src/core/types/ai-table.ts @@ -9,10 +9,11 @@ export interface AITable { fields: WritableSignal; context?: RendererContext; selection: WritableSignal; + matchedCells: WritableSignal; // [`${recordId}-${fieldId}`] recordsMap: Signal<{ [kay: string]: AITableRecord }>; fieldsMap: Signal<{ [kay: string]: AITableField }>; recordsWillHidden: WritableSignal; - recordsWillMove: WritableSignal + recordsWillMove: WritableSignal; } export type AIPlugin = (aiTable: AITable) => AITable; diff --git a/packages/grid/src/core/utils/common.ts b/packages/grid/src/core/utils/common.ts index b017682b..5862daee 100644 --- a/packages/grid/src/core/utils/common.ts +++ b/packages/grid/src/core/utils/common.ts @@ -10,6 +10,7 @@ export function createAITable(records: WritableSignal, fields: W selectedFields: new Map(), selectedCells: new Map() }), + matchedCells: signal([]), recordsMap: computed(() => { return records().reduce( (object, item) => { diff --git a/packages/grid/src/grid-base.component.ts b/packages/grid/src/grid-base.component.ts index a34c4be6..251e9520 100644 --- a/packages/grid/src/grid-base.component.ts +++ b/packages/grid/src/grid-base.component.ts @@ -35,6 +35,7 @@ import { AI_TABLE_GRID_FIELD_SERVICE_MAP, AITableGridFieldService } from './serv import { AITableGridSelectionService } from './services/selection.service'; import { AIFieldConfig, AITableFieldMenuItem, AITableContextMenuItem, AITableReferences } from './types'; import { AITableFieldPropertyEditor } from './components'; +import { AITableGridMatchCellService } from './services/match-cell.service'; @Component({ selector: 'ai-table-grid-base', @@ -59,6 +60,8 @@ export class AITableGridBase implements OnInit { aiBuildRenderDataFn = input<(aiTable: AITable) => AITableValue>(); + aiKeywords = input(); + AITableFieldType = AITableFieldType; AITableSelectOptionStyle = AITableSelectOptionStyle; @@ -99,6 +102,7 @@ export class AITableGridBase implements OnInit { protected aiTableGridFieldService = inject(AITableGridFieldService); protected aiTableGridEventService = inject(AITableGridEventService); protected aiTableGridSelectionService = inject(AITableGridSelectionService); + protected aiTableGridMatchCellService = inject(AITableGridMatchCellService); ngOnInit(): void { this.initAITable(); @@ -116,6 +120,7 @@ export class AITableGridBase implements OnInit { initService() { this.aiTableGridEventService.initialize(this.aiTable, this.aiFieldConfig()?.fieldPropertyEditor); this.aiTableGridSelectionService.initialize(this.aiTable); + this.aiTableGridMatchCellService.initialize(this.aiTable); this.aiTableGridEventService.registerEvents(this.elementRef.nativeElement); this.aiTableGridFieldService.initAIFieldConfig(this.aiFieldConfig()); AI_TABLE_GRID_FIELD_SERVICE_MAP.set(this.aiTable, this.aiTableGridFieldService); diff --git a/packages/grid/src/grid.component.ts b/packages/grid/src/grid.component.ts index cd7cbb1c..e2ed7a0f 100644 --- a/packages/grid/src/grid.component.ts +++ b/packages/grid/src/grid.component.ts @@ -43,6 +43,7 @@ import { AITableGridSelectionService } from './services/selection.service'; import { AITableMouseDownType, AITableRendererConfig, ScrollActionOptions } from './types'; import { buildGridLinearRows, getColumnIndicesMap, getDetailByTargetName, handleMouseStyle, isWindows } from './utils'; import { getMousePosition } from './utils/position'; +import { AITableGridMatchCellService } from './services/match-cell.service'; @Component({ selector: 'ai-table-grid', @@ -53,7 +54,7 @@ import { getMousePosition } from './utils/position'; class: 'ai-table-grid' }, imports: [AITableRenderer], - providers: [AITableGridEventService, AITableGridFieldService, AITableGridSelectionService] + providers: [AITableGridEventService, AITableGridFieldService, AITableGridSelectionService, AITableGridMatchCellService] }) export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy { private viewContainerRef = inject(ViewContainerRef); @@ -147,6 +148,14 @@ export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy { this.toggleHoverCellEditor(); } }); + effect( + () => { + if (this.aiKeywords()) { + this.aiTableGridMatchCellService.findMatchedCells(this.aiKeywords()!, this.aiReferences()); + } + }, + { allowSignalWrites: true } + ); } override ngOnInit(): void { diff --git a/packages/grid/src/renderer/creations/create-cells.ts b/packages/grid/src/renderer/creations/create-cells.ts index 6499d7a9..13b50602 100644 --- a/packages/grid/src/renderer/creations/create-cells.ts +++ b/packages/grid/src/renderer/creations/create-cells.ts @@ -6,7 +6,7 @@ import { DEFAULT_FONT_STYLE } from '../../constants'; import { AITable, AITableQueries, RendererContext } from '../../core'; -import { AITableAreaType, AITableCellsDrawerConfig, AITableRender, AITableRowType } from '../../types'; +import { AITableCellsDrawerConfig, AITableRender, AITableRowType } from '../../types'; import { getCellHorizontalPosition, transformCellValue } from '../../utils'; import { addRowLayout } from '../drawers/add-row-layout-drawer'; import { cellDrawer } from '../drawers/cell-drawer'; @@ -85,7 +85,15 @@ export const createCells = (config: AITableCellsDrawerConfig) => { const isSiblingActiveCell = recordId === activeCell?.recordId && fieldId !== activeCell?.fieldId; const isActiveCell = recordId === activeCell?.recordId; - if (isCheckedRow || isSelected || isSiblingActiveCell) { + let matchedCellsMap: { [key: string]: boolean } = {}; + aiTable.matchedCells().forEach((key) => { + matchedCellsMap[key] = true; + }); + const isMatchedCell = matchedCellsMap[`${recordId}-${fieldId}`]; + + if (isMatchedCell) { + background = colors.itemMatchBgColor; + } else if (isCheckedRow || isSelected || isSiblingActiveCell) { background = colors.itemActiveBgColor; } else if (isHoverRow && !isActiveCell) { background = colors.gray80; diff --git a/packages/grid/src/services/index.ts b/packages/grid/src/services/index.ts index 6a76618d..d2764b76 100644 --- a/packages/grid/src/services/index.ts +++ b/packages/grid/src/services/index.ts @@ -1,3 +1,4 @@ export * from './event.service'; export * from './field.service'; export * from './selection.service'; +export * from './match-cell.service'; diff --git a/packages/grid/src/services/match-cell.service.ts b/packages/grid/src/services/match-cell.service.ts new file mode 100644 index 00000000..210552c3 --- /dev/null +++ b/packages/grid/src/services/match-cell.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { AITable, AITableField, AITableQueries } from '../core'; +import { AITableReferences } from '../types'; +import { transformCellValue } from '../utils'; +import { ViewOperationMap } from '@ai-table/state'; + +@Injectable() +export class AITableGridMatchCellService { + aiTable!: AITable; + + initialize(aiTable: AITable) { + this.aiTable = aiTable; + } + + findMatchedCells(keywords: string, references: AITableReferences) { + let matchedCells: string[] = []; + this.aiTable.records().forEach((record) => { + this.aiTable.fields().forEach((field) => { + if (this.isCellMatchKeywords(this.aiTable, field, record._id, keywords, references)) { + matchedCells.push(`${record._id}-${field._id}`); + } + }); + }); + this.aiTable.matchedCells.set([...matchedCells]); + } + + private isCellMatchKeywords(aiTable: AITable, field: AITableField, recordId: string, keywords: string, references: AITableReferences) { + const cellValue = AITableQueries.getFieldValue(aiTable, [recordId, field._id]); + const transformValue = transformCellValue(aiTable, field, cellValue); + const fieldMethod = ViewOperationMap[field.type]; + let cellFullText: string[] = fieldMethod.cellFullText(transformValue, field, references); + + try { + return keywords && cellFullText.length && cellFullText.some((item) => item.toLowerCase().includes(keywords.toLowerCase())); + } catch (error) { + return false; + } + } +} diff --git a/packages/state/src/types/view.ts b/packages/state/src/types/view.ts index fd759dd8..2a702247 100644 --- a/packages/state/src/types/view.ts +++ b/packages/state/src/types/view.ts @@ -49,7 +49,7 @@ export enum AITableFilterOperation { notContain = 'not_contain' } -export type ViewSettings = AITableFilterConditions & AITableSortOptions; +export type ViewSettings = AITableSearchOptions & AITableFilterConditions & AITableSortOptions; export interface AITableView { _id: string; @@ -78,4 +78,8 @@ export interface AITableSortOptions { }[]; } +export interface AITableSearchOptions { + keywords?: string; +} + export type AITableViews = AITableView[]; diff --git a/packages/state/src/utils/field/model/field.ts b/packages/state/src/utils/field/model/field.ts index fc2e8ce3..80d3e425 100644 --- a/packages/state/src/utils/field/model/field.ts +++ b/packages/state/src/utils/field/model/field.ts @@ -1,4 +1,4 @@ -import { AITableField, FieldValue } from '@ai-table/grid'; +import { AITableField, AITableReferences, FieldValue } from '@ai-table/grid'; import { AITableFilterCondition, AITableFilterOperation } from '../../../types'; import { isEmpty } from '../../common'; import { isEqual } from 'lodash'; @@ -72,4 +72,12 @@ export abstract class Field { // test pinyin sort return str1 === str2 ? 0 : zhIntlCollator ? zhIntlCollator.compare(str1, str2) : str1.localeCompare(str2, 'zh-CN') > 0 ? 1 : -1; } + + cellFullText(transformValue: any, field: AITableField, references?: AITableReferences): string[] { + let fullText: string[] = []; + if (!isEmpty(transformValue)) { + fullText.push(String(transformValue)); + } + return fullText; + } } diff --git a/packages/state/src/utils/field/model/index.ts b/packages/state/src/utils/field/model/index.ts index deb3ba15..d2f48f41 100644 --- a/packages/state/src/utils/field/model/index.ts +++ b/packages/state/src/utils/field/model/index.ts @@ -6,6 +6,7 @@ import { DateField } from './date'; import { NumberField } from './number'; import { RateField } from './rate'; import { LinkField } from './link'; +import { MemberField } from './member'; export const ViewOperationMap: Record = { [AITableFieldType.text]: new TextField(), @@ -17,8 +18,8 @@ export const ViewOperationMap: Record = { [AITableFieldType.number]: new NumberField(), [AITableFieldType.rate]: new RateField(), [AITableFieldType.link]: new LinkField(), - [AITableFieldType.member]: new SelectField(), + [AITableFieldType.member]: new MemberField(), [AITableFieldType.progress]: new NumberField(), - [AITableFieldType.createdBy]: new SelectField(), - [AITableFieldType.updatedBy]: new SelectField() + [AITableFieldType.createdBy]: new MemberField(), + [AITableFieldType.updatedBy]: new MemberField() }; diff --git a/packages/state/src/utils/field/model/link.ts b/packages/state/src/utils/field/model/link.ts index 735049d3..676d9e00 100644 --- a/packages/state/src/utils/field/model/link.ts +++ b/packages/state/src/utils/field/model/link.ts @@ -39,4 +39,12 @@ export class LinkField extends Field { } return ''; } + + override cellFullText(transformValue: LinkFieldValue, field: AITableField): string[] { + let fullText: string[] = []; + if (!isEmpty(transformValue?.text)) { + fullText.push(transformValue.text); + } + return fullText; + } } diff --git a/packages/state/src/utils/field/model/member.ts b/packages/state/src/utils/field/model/member.ts new file mode 100644 index 00000000..f3014a5b --- /dev/null +++ b/packages/state/src/utils/field/model/member.ts @@ -0,0 +1,20 @@ +import { AITableField, AITableReferences } from '@ai-table/grid'; +import { SelectField } from './select'; + +export class MemberField extends SelectField { + override cellFullText(transformValue: string[], field: AITableField, references?: AITableReferences): string[] { + let fullText: string[] = []; + if (transformValue?.length && references) { + for (let index = 0; index < transformValue.length; index++) { + const userInfo = references?.members[transformValue[index]]; + if (!userInfo) { + continue; + } + if (userInfo.display_name) { + fullText.push(userInfo.display_name); + } + } + } + return fullText; + } +} diff --git a/packages/state/src/utils/field/model/progress.ts b/packages/state/src/utils/field/model/progress.ts new file mode 100644 index 00000000..6d8d92fd --- /dev/null +++ b/packages/state/src/utils/field/model/progress.ts @@ -0,0 +1,9 @@ +import { AITableField } from '@ai-table/grid'; +import { NumberField } from './number'; + +export class ProgressField extends NumberField { + override cellFullText(transformValue: number, field: AITableField): string[] { + const fullText = `${transformValue}%`; + return [fullText]; + } +} diff --git a/packages/state/src/utils/field/model/select.ts b/packages/state/src/utils/field/model/select.ts index d4f6a2db..e872369f 100644 --- a/packages/state/src/utils/field/model/select.ts +++ b/packages/state/src/utils/field/model/select.ts @@ -1,7 +1,7 @@ import { isEmpty } from '../../common'; import { AITableFilterCondition, AITableFilterOperation } from '../../../types'; import { Field } from './field'; -import { AITableField, AITableSelectOption, SelectFieldValue, SelectSettings } from '@ai-table/grid'; +import { AITableField, AITableSelectField, AITableSelectOption, SelectFieldValue, SelectSettings } from '@ai-table/grid'; import { Id } from 'ngx-tethys/types'; export class SelectField extends Field { @@ -39,12 +39,25 @@ export class SelectField extends Field { } findOptionById(field: AITableField, id: Id): AITableSelectOption | null { - return (field.settings as SelectSettings).options.find(option => option._id === id) || null; + return (field.settings as SelectSettings).options.find((option) => option._id === id) || null; } arrayValueToString(cellValues: string[] | null): string | null { return cellValues && cellValues.length ? cellValues.join(', ') : null; } + + override cellFullText(transformValue: string[], field: AITableField): string[] { + let cellText: string[] = []; + if (transformValue && Array.isArray(transformValue) && transformValue.length) { + transformValue.forEach((optionId) => { + const item = (field as AITableSelectField).settings?.options?.find((option) => option._id === optionId); + if (item?.text) { + cellText.push(item.text); + } + }); + } + return cellText; + } } function hasIntersect(array1: T[], array2: T[]) { diff --git a/src/app/component/common/content/content.component.html b/src/app/component/common/content/content.component.html index 4a7aa5c7..d78cd75a 100644 --- a/src/app/component/common/content/content.component.html +++ b/src/app/component/common/content/content.component.html @@ -33,6 +33,7 @@ [(aiRecords)]="tableService.records" [(aiFields)]="tableService.fields" [aiFieldConfig]="aiFieldConfig()" + [aiKeywords]="tableService.keywords()" [aiPlugins]="plugins" [aiReferences]="references()" (aiAddRecord)="addRecord($event)" diff --git a/src/app/service/table.service.ts b/src/app/service/table.service.ts index cb974524..7cd0c8cc 100644 --- a/src/app/service/table.service.ts +++ b/src/app/service/table.service.ts @@ -69,6 +69,10 @@ export class TableService { }; }); + keywords = computed(() => { + return this.activeView().settings?.keywords; + }); + initData(views: AITableView[]) { this.views = signal(views); }