UNPKG

@progress/kendo-angular-grid

Version:

Kendo UI Grid for Angular - high performance data grid with paging, filtering, virtualization, CRUD, and more.

625 lines (624 loc) 26.3 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Injectable, NgZone } from '@angular/core'; import { isPresent } from '@progress/kendo-angular-common'; import { ColumnInfoService } from './../../../../common/column-info.service'; import { ContextService } from './../../../../common/provider.service'; import { convertDateStringsInFilter, highlightBy } from './utils'; import { isCheckboxColumn } from '../../../../columns/column-base'; import { CommandColumnComponent } from '../../../../columns/command-column.component'; import * as i0 from "@angular/core"; import * as i1 from "./../../../../common/provider.service"; import * as i2 from "./../../../../common/column-info.service"; /** * @hidden * * Service that builds AI requests and processes AI responses for the Grid. * Used internally by both the Grid component and the AI Assistant tool. */ export class GridAIRequestResponseService { ctx; columnInfoService; zone; constructor(ctx, columnInfoService, zone) { this.ctx = ctx; this.columnInfoService = columnInfoService; this.zone = zone; } /** * Builds the request body for the AI service based on the Grid's column structure. * Returns a column descriptor tree that includes column metadata for the AI service. */ buildRequestBody(promptMessage, role) { const columnsTree = this.buildColumnDescriptors(); return { role: role || 'user', contents: [ { text: promptMessage } ], columns: columnsTree }; } /** * Builds a nested column descriptor tree based on the Grid's column structure. * Includes root columns and their nested children (for ColumnGroup and SpanColumn). */ buildColumnDescriptors() { const rootColumns = this.ctx?.grid?.columnList?.rootColumns() || []; const buildDescriptor = (col) => { const hasChildren = Boolean(col.hasChildren && col.childrenArray?.length); const descriptor = { id: col.id, field: col.field, header: col.title }; if (hasChildren) { descriptor.columns = col.childrenArray.map((c) => buildDescriptor(c)); } // For special columns that don't have a field, emit an optional type token // so the AI service knows how to treat them (checkbox/command/reorder) if (!col.field) { if (isCheckboxColumn(col)) { descriptor.type = 'checkbox'; } else if (col instanceof CommandColumnComponent) { descriptor.type = 'command'; } } return descriptor; }; return rootColumns.map((col) => buildDescriptor(col)); } /** * Processes AI response commands and applies them to the Grid. * Returns an array of display messages for each command. */ processCommands(commands, columns, leafColumns) { const messages = []; this.executeCommands(commands || [], columns, leafColumns, messages); return messages; } executeCommands(commands, columns, leafColumns, messages) { if (!commands?.length) { return; } const grid = this.ctx.grid; const isFilterable = Boolean(grid.filterable); const isSortable = Boolean(grid.sortable); const isGroupable = Boolean(grid.groupable); const findColumnById = (id) => grid.columnList.toArray().find((c) => c.id === id); const updateColumnHierarchy = (column, updater) => { const changed = []; const queue = [column]; while (queue.length) { const current = queue.shift(); if (!current) { continue; } const didChange = updater(current); if (didChange) { changed.push(current); } if (current.hasChildren && current.childrenArray?.length) { queue.push(...current.childrenArray); } } return changed; }; commands.forEach((cmd) => { let displayMessage = cmd.message || ''; if (this.isColumnCommand(cmd.type)) { if (cmd.id) { const column = findColumnById(cmd.id); const replacement = this.getColumnReplacement(column); displayMessage = this.replaceQuotedColumnId(displayMessage, replacement); } } messages.push(displayMessage); switch (cmd.type) { case 'GridSort': if (!isSortable) { break; } this.processArrayResponse([cmd.sort], grid.currentState.sort || [], (item) => item.field, (mergedArray) => grid.sortChange.next(mergedArray)); break; case 'GridClearSort': if (!isSortable) { break; } grid.sortChange.next([]); break; case 'GridFilter': if (!isFilterable) { break; } this.processFilterResponse(cmd.filter); break; case 'GridClearFilter': if (!isFilterable) { break; } grid.filterChange.next(undefined); break; case 'GridGroup': if (!isGroupable) { break; } this.processArrayResponse([cmd.group], grid.currentState.group || [], (item) => item.field, (mergedArray) => grid.groupChange.next(mergedArray)); break; case 'GridClearGroup': if (!isGroupable) { break; } grid.groupChange.next([]); break; case 'GridHighlight': if (!this.ctx.highlightDirective) { break; } this.processHighlightResponse([cmd.highlight], columns); break; case 'GridClearHighlight': if (!this.ctx.highlightDirective) { break; } this.ctx.highlightDirective['setState']([]); break; case 'GridSelect': { this.processSelectionResponse([cmd.select], columns, leafColumns, messages); break; } case 'GridClearSelect': { const selectionInstance = this.getSelectionInstance(); if (!selectionInstance) { this.updateLastMessage(messages, this.ctx.localization?.get('aiAssistantSelectionNotEnabled')); break; } this.applySelectionState(selectionInstance, []); break; } case 'GridColumnResize': { const col = findColumnById(cmd.id); if (!col) { break; } let newWidth; if (typeof cmd.size === 'number') { newWidth = cmd.size; } else if (typeof cmd.size === 'string') { const numericPart = parseFloat(cmd.size); if (!isNaN(numericPart)) { newWidth = numericPart; } } if (typeof newWidth === 'number') { const oldWidth = col.width; col.width = newWidth; const args = [{ column: col, oldWidth: oldWidth, newWidth: newWidth }]; grid.columnResize.emit(args); } break; } case 'GridColumnReorder': { const col = findColumnById(cmd.id); if (!col) { break; } const newPosition = Number(cmd.position); if (!isNaN(newPosition) && newPosition >= 0) { this.changeColumnPosition(col, newPosition); } break; } case 'GridColumnShow': case 'GridColumnHide': { const col = findColumnById(cmd.id); if (!col) { break; } const targetHidden = cmd.type === 'GridColumnHide'; const changed = updateColumnHierarchy(col, (current) => { if (current.hidden === targetHidden) { return false; } current.hidden = targetHidden; return true; }); if (changed.length) { this.columnInfoService.changeVisibility(changed); } break; } case 'GridColumnLock': case 'GridColumnUnlock': { const col = findColumnById(cmd.id); if (!col) { break; } const targetLocked = cmd.type === 'GridColumnLock'; const changed = updateColumnHierarchy(col, (current) => { if (current.locked === targetLocked) { return false; } current.locked = targetLocked; return true; }); if (changed.length) { this.columnInfoService.changeLocked(changed); } break; } case 'GridPage': { this.processPageCommand(cmd); break; } case 'GridPageSize': { this.processPageSizeCommand(cmd); break; } case 'GridExportExcel': { this.runExportWithFileName(this.ctx.excelComponent, cmd.fileName, () => grid.saveAsExcel()); break; } case 'GridExportPDF': { this.runExportWithFileName(this.ctx.pdfComponent, cmd.fileName, () => grid.emitPDFExportEvent()); break; } default: break; } }); } processArrayResponse(newItems, currentItems, getField, updateGrid) { if (newItems?.length === 0) { updateGrid([]); } else if (newItems?.length) { let mergedArray = [...newItems]; const newFields = newItems.map(getField); const existingItemsToKeep = currentItems.filter(item => !newFields.includes(getField(item))); mergedArray = [...mergedArray, ...existingItemsToKeep]; updateGrid(mergedArray); } } runExportWithFileName(component, fileName, action) { if (!component || !fileName) { action(); return; } const previousFileName = component.fileName; component.fileName = fileName; action(); const isExcel = component === this.ctx.excelComponent; if (isExcel) { this.zone.runOutsideAngular(() => { this.ctx.excelComponent.fileCreated.subscribe(() => { component.fileName = previousFileName; }); }); } else { component.fileName = previousFileName; } } processPageCommand(command) { const pageSize = this.getCurrentPageSizeValue(); if (!isPresent(pageSize) || pageSize <= 0) { return; } const total = this.getTotalItemsCount(); const requestedPage = Number(command.page); let targetPage = Number.isFinite(requestedPage) ? Math.floor(requestedPage) : 1; if (targetPage < 1) { targetPage = 1; } if (isPresent(total) && pageSize > 0) { const maxPage = Math.max(1, Math.ceil(total / pageSize)); targetPage = Math.min(targetPage, maxPage); } const skip = (targetPage - 1) * pageSize; this.emitGridPageChange(skip, pageSize); } processPageSizeCommand(command) { const rawPageSize = Number(command.pageSize); if (!Number.isFinite(rawPageSize)) { return; } const newPageSize = Math.max(1, Math.floor(rawPageSize)); const skip = Math.max(0, this.ctx.grid?.skip ?? 0); this.ensurePageSizeOption(newPageSize); this.emitGridPageChange(skip, newPageSize); } emitGridPageChange(skip, take) { const grid = this.ctx.grid; const normalizedSkip = Math.max(0, Math.floor(skip)); const normalizedTake = Math.max(1, Math.floor(take)); grid.skip = normalizedSkip; grid.pageSize = normalizedTake; grid.pageChange.emit({ skip: normalizedSkip, take: normalizedTake }); } ensurePageSizeOption(pageSize) { const grid = this.ctx.grid; if (!grid) { return; } const pageable = grid.pageable; if (!pageable || typeof pageable === 'boolean') { return; } const pageSizes = pageable.pageSizes; if (!Array.isArray(pageSizes) || pageSizes.length === 0) { return; } if (pageSizes.includes(pageSize)) { return; } const uniqueSizes = [pageSize, ...pageSizes.filter(size => size !== pageSize)]; grid.pageable = { ...pageable, pageSizes: uniqueSizes }; const changeDetector = grid?.changeDetectorRef; if (changeDetector && typeof changeDetector.markForCheck === 'function') { changeDetector.markForCheck(); } } getCurrentPageSizeValue() { const grid = this.ctx.grid; if (!grid) { return null; } const candidates = [grid.pageSize, grid.currentState?.take, this.ctx.dataBindingDirective?.['state']?.take]; for (const candidate of candidates) { if (typeof candidate === 'number' && candidate > 0) { return candidate; } } const pageable = grid.pageable; if (pageable && typeof pageable === 'object' && Array.isArray(pageable.pageSizes)) { const numericSize = pageable.pageSizes.find(size => typeof size === 'number' && size > 0); if (numericSize) { return numericSize; } } const originalData = this.ctx.dataBindingDirective?.['originalData']; if (Array.isArray(originalData) && originalData.length > 0) { return originalData.length; } return null; } getTotalItemsCount() { const grid = this.ctx.grid; if (!grid) { return null; } const gridData = grid.data; if (gridData && typeof gridData.total === 'number') { return gridData.total; } const view = grid.view; if (view && typeof view.total === 'number') { return view.total; } const originalData = this.ctx.dataBindingDirective?.['originalData']; if (Array.isArray(originalData)) { return originalData.length; } return null; } getSelectionInstance() { const selectionDirective = this.ctx.grid?.selectionDirective; if (selectionDirective && typeof selectionDirective === 'object') { return selectionDirective; } const defaultSelection = this.ctx.grid?.defaultSelection; if (defaultSelection && typeof defaultSelection === 'object') { return defaultSelection; } return null; } updateLastMessage(messages, newMessage) { if (!messages.length) { return; } messages[messages.length - 1] = newMessage; } isColumnCommand(type) { return type === 'GridColumnResize' || type === 'GridColumnReorder' || type === 'GridColumnShow' || type === 'GridColumnHide' || type === 'GridColumnLock' || type === 'GridColumnUnlock'; } getColumnReplacement(column) { if (!column) { return ''; } if (column.title && String(column.title).trim()) { return String(column.title).trim(); } if (column.field && String(column.field).trim()) { return String(column.field).trim(); } return ''; } replaceQuotedColumnId(message, replacement) { if (!replacement) { const columnIdPattern = /(?:&quot;|")(k-grid\d+-col\d+)(?:&quot;|")\s*/g; return message.replace(columnIdPattern, '').replace(/\s{2,}/g, ' ').trim(); } const columnIdPattern = /(?:&quot;|")(k-grid\d+-col\d+)(?:&quot;|")/g; return message.replace(columnIdPattern, (match) => { const isEncoded = match.startsWith('&quot;'); return isEncoded ? `&quot;${replacement}&quot;` : `"${replacement}"`; }); } getHighlightItems(descriptors, columns) { if (!descriptors?.length) { return []; } const data = this.ctx.dataBindingDirective?.['originalData'] || []; return highlightBy(data, descriptors, columns); } processSelectionResponse(selection, columns, leafColumns, messages) { const selectionInstance = this.getSelectionInstance(); if (!selectionInstance) { this.updateLastMessage(messages, this.ctx.localization?.get('aiAssistantSelectionNotEnabled')); return; } const descriptors = (selection || []).filter((descriptor) => Boolean(descriptor)); if (descriptors.length === 0) { this.applySelectionState(selectionInstance, []); return; } const highlightItems = this.getHighlightItems(descriptors, columns); if (!highlightItems.length) { this.applySelectionState(selectionInstance, []); return; } const hasCellSelections = highlightItems.some(item => isPresent(item.columnKey)); const hasRowSelections = highlightItems.some(item => !isPresent(item.columnKey)); const isCellMode = selectionInstance.isCellSelectionMode; if ((!isCellMode && hasCellSelections) || (isCellMode && hasRowSelections)) { const key = isCellMode ? 'aiAssistantSelectionRowModeRequired' : 'aiAssistantSelectionCellModeRequired'; this.updateLastMessage(messages, this.ctx.localization?.get(key)); return; } const selectionState = this.mapHighlightItemsToSelection(selectionInstance, highlightItems, isCellMode, leafColumns); this.applySelectionState(selectionInstance, selectionState); } mapHighlightItemsToSelection(selectionInstance, highlightItems, isCellMode, leafColumns) { const data = this.ctx.dataBindingDirective?.['originalData'] || []; if (isCellMode) { const mapped = highlightItems .filter(item => isPresent(item.itemKey) && isPresent(item.columnKey)) .map(item => { const rowIndex = item.itemKey; const columnIndex = item.columnKey; const dataItem = data[rowIndex]; if (!isPresent(dataItem)) { return null; } if (typeof selectionInstance['getSelectionItem'] === 'function') { const columnComponent = leafColumns[columnIndex]; const selectionItem = selectionInstance['getSelectionItem']({ dataItem, index: rowIndex }, columnComponent, columnIndex); if (selectionItem && isPresent(selectionItem.itemKey) && isPresent(selectionItem.columnKey)) { return selectionItem; } return null; } const itemKey = typeof selectionInstance.getItemKey === 'function' ? selectionInstance.getItemKey({ dataItem, index: rowIndex }) : rowIndex; return isPresent(itemKey) ? { itemKey, columnKey: columnIndex } : null; }) .filter((item) => isPresent(item)); return mapped.filter((item, index, self) => self.findIndex(other => other.itemKey === item.itemKey && other.columnKey === item.columnKey) === index); } const rowKeys = highlightItems .filter(item => isPresent(item.itemKey)) .map(item => { const rowIndex = item.itemKey; const dataItem = data[rowIndex]; if (!isPresent(dataItem)) { return null; } if (typeof selectionInstance.getItemKey === 'function') { return selectionInstance.getItemKey({ dataItem, index: rowIndex }); } return rowIndex; }) .filter(isPresent); return Array.from(new Set(rowKeys)); } applySelectionState(selectionInstance, selectionState) { selectionInstance.selectedKeys = selectionState; if (typeof selectionInstance['setState'] === 'function') { selectionInstance['setState'](selectionState); } const changeDetector = selectionInstance['cd']; if (changeDetector && typeof changeDetector.markForCheck === 'function') { changeDetector.markForCheck(); } if (typeof selectionInstance['notifyChange'] === 'function') { selectionInstance['notifyChange'](); } } processHighlightResponse(highlight, columns) { const highlightedItems = this.getHighlightItems(highlight, columns); this.ctx.highlightDirective['setState'](highlightedItems); } processFilterResponse(filter) { const processedFilter = convertDateStringsInFilter(filter); const clearFilter = Object.keys(processedFilter).length === 0; if (clearFilter) { this.ctx.grid.filterChange.next(undefined); } else if (processedFilter?.filters.length) { const currentFilter = this.ctx.grid.currentState.filter; let mergedFilter = processedFilter; if (currentFilter && currentFilter.filters?.length > 0) { mergedFilter = { logic: 'and', filters: [ currentFilter, processedFilter ] }; } this.ctx.grid.filterChange.next(mergedFilter); } } changeColumnPosition(column, newPosition) { const grid = this.ctx.grid; if (!grid?.columns) { return; } const currentColumns = grid.columns.toArray(); const currentIndex = currentColumns.findIndex(col => col === column); if (currentIndex === -1) { return; } if (newPosition < 0 || newPosition >= currentColumns.length) { return; } const sortedColumns = currentColumns .map((col, idx) => ({ col, physicalIndex: idx, visualOrder: col.orderIndex ?? idx })) .sort((a, b) => a.visualOrder - b.visualOrder); const currentVisualPos = sortedColumns.findIndex(item => item.physicalIndex === currentIndex); if (currentVisualPos === newPosition) { return; } currentColumns.forEach((col, idx) => { const sortedIndex = sortedColumns.findIndex(item => item.physicalIndex === idx); if (idx === currentIndex) { col.orderIndex = newPosition; } else if (currentVisualPos < newPosition) { col.orderIndex = (sortedIndex > currentVisualPos && sortedIndex <= newPosition) ? sortedIndex - 1 : sortedIndex; } else { col.orderIndex = (sortedIndex >= newPosition && sortedIndex < currentVisualPos) ? sortedIndex + 1 : sortedIndex; } col.isReordered = true; }); grid.columnReorder.emit({ column: column, oldIndex: currentVisualPos, newIndex: newPosition }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridAIRequestResponseService, deps: [{ token: i1.ContextService }, { token: i2.ColumnInfoService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridAIRequestResponseService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridAIRequestResponseService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.ContextService }, { type: i2.ColumnInfoService }, { type: i0.NgZone }] });