Use AI assistant to create records in the system, added configurable list views

This commit is contained in:
Francisco Gaona
2026-01-31 03:24:46 +01:00
parent 20fc90a3fb
commit f68321c802
18 changed files with 3310 additions and 142 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,15 +12,94 @@ export interface AiChatContext {
export interface AiAssistantReply {
reply: string;
action?: 'create_record' | 'collect_fields' | 'clarify';
action?: 'create_record' | 'collect_fields' | 'clarify' | 'plan_complete' | 'plan_pending';
missingFields?: string[];
record?: any;
records?: any[]; // Multiple records when plan execution completes
plan?: RecordCreationPlan;
}
// ============================================
// Entity Discovery Types
// ============================================
export interface EntityFieldInfo {
apiName: string;
label: string;
type: string;
isRequired: boolean;
isSystem: boolean;
referenceObject?: string; // For LOOKUP fields, the target entity
description?: string;
}
export interface EntityRelationship {
fieldApiName: string;
fieldLabel: string;
targetEntity: string;
relationshipType: 'lookup' | 'master-detail' | 'polymorphic';
}
export interface EntityInfo {
apiName: string;
label: string;
pluralLabel?: string;
description?: string;
fields: EntityFieldInfo[];
requiredFields: string[]; // Field apiNames that are required
relationships: EntityRelationship[];
}
export interface SystemEntities {
entities: EntityInfo[];
entityByApiName: Record<string, EntityInfo>; // Changed from Map for state serialization
loadedAt: number;
}
// ============================================
// Planning Types
// ============================================
export interface PlannedRecord {
id: string; // Temporary ID for planning (e.g., "temp_account_1")
entityApiName: string;
entityLabel: string;
fields: Record<string, any>;
resolvedFields?: Record<string, any>; // Fields after dependency resolution
missingRequiredFields: string[];
dependsOn: string[]; // IDs of other planned records this depends on
status: 'pending' | 'ready' | 'created' | 'failed';
createdRecordId?: string; // Actual ID after creation
wasExisting?: boolean; // True if record already existed in database
error?: string;
}
export interface RecordCreationPlan {
id: string;
records: PlannedRecord[];
executionOrder: string[]; // Ordered list of planned record IDs
status: 'building' | 'incomplete' | 'ready' | 'executing' | 'completed' | 'failed';
createdRecords: any[];
errors: string[];
}
// ============================================
// State Types
// ============================================
export interface AiAssistantState {
message: string;
messages?: any[]; // BaseMessage[] from langchain - used when invoked by Deep Agent
history?: AiChatMessage[];
context: AiChatContext;
// Entity discovery
systemEntities?: SystemEntities;
// Planning
plan?: RecordCreationPlan;
// Legacy fields (kept for compatibility during transition)
objectDefinition?: any;
pageLayout?: any;
extractedFields?: Record<string, any>;

View File

@@ -79,6 +79,10 @@ export class FieldMapperService {
const frontendType = this.mapFieldType(field.type);
const isLookupField = frontendType === 'belongsTo' || field.type.toLowerCase().includes('lookup');
// Hide 'id' field from list view by default
const isIdField = field.apiName === 'id';
const defaultShowOnList = isIdField ? false : true;
return {
id: field.id,
apiName: field.apiName,
@@ -95,7 +99,7 @@ export class FieldMapperService {
isReadOnly: field.isSystem || uiMetadata.isReadOnly || false,
// View visibility
showOnList: uiMetadata.showOnList !== false,
showOnList: uiMetadata.showOnList !== undefined ? uiMetadata.showOnList : defaultShowOnList,
showOnDetail: uiMetadata.showOnDetail !== false,
showOnEdit: uiMetadata.showOnEdit !== false && !field.isSystem,
sortable: uiMetadata.sortable !== false,
@@ -141,6 +145,7 @@ export class FieldMapperService {
'boolean': 'boolean',
'date': 'date',
'datetime': 'datetime',
'date_time': 'datetime',
'time': 'time',
'email': 'email',
'url': 'url',

View File

@@ -1,4 +1,6 @@
import { IsString, IsUUID, IsBoolean, IsOptional, IsObject } from 'class-validator';
import { IsString, IsUUID, IsBoolean, IsOptional, IsObject, IsIn } from 'class-validator';
export type PageLayoutType = 'detail' | 'list';
export class CreatePageLayoutDto {
@IsString()
@@ -7,18 +9,25 @@ export class CreatePageLayoutDto {
@IsUUID()
objectId: string;
@IsIn(['detail', 'list'])
@IsOptional()
layoutType?: PageLayoutType = 'detail';
@IsBoolean()
@IsOptional()
isDefault?: boolean;
@IsObject()
layoutConfig: {
// For detail layouts: grid-based field positions
fields: Array<{
fieldId: string;
x: number;
y: number;
w: number;
h: number;
x?: number;
y?: number;
w?: number;
h?: number;
// For list layouts: field order (optional, defaults to array index)
order?: number;
}>;
relatedLists?: string[];
};
@@ -42,10 +51,11 @@ export class UpdatePageLayoutDto {
layoutConfig?: {
fields: Array<{
fieldId: string;
x: number;
y: number;
w: number;
h: number;
x?: number;
y?: number;
w?: number;
h?: number;
order?: number;
}>;
relatedLists?: string[];
};

View File

@@ -10,7 +10,7 @@ import {
Query,
} from '@nestjs/common';
import { PageLayoutService } from './page-layout.service';
import { CreatePageLayoutDto, UpdatePageLayoutDto } from './dto/page-layout.dto';
import { CreatePageLayoutDto, UpdatePageLayoutDto, PageLayoutType } from './dto/page-layout.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { TenantId } from '../tenant/tenant.decorator';
@@ -25,13 +25,21 @@ export class PageLayoutController {
}
@Get()
findAll(@TenantId() tenantId: string, @Query('objectId') objectId?: string) {
return this.pageLayoutService.findAll(tenantId, objectId);
findAll(
@TenantId() tenantId: string,
@Query('objectId') objectId?: string,
@Query('layoutType') layoutType?: PageLayoutType,
) {
return this.pageLayoutService.findAll(tenantId, objectId, layoutType);
}
@Get('default/:objectId')
findDefaultByObject(@TenantId() tenantId: string, @Param('objectId') objectId: string) {
return this.pageLayoutService.findDefaultByObject(tenantId, objectId);
findDefaultByObject(
@TenantId() tenantId: string,
@Param('objectId') objectId: string,
@Query('layoutType') layoutType?: PageLayoutType,
) {
return this.pageLayoutService.findDefaultByObject(tenantId, objectId, layoutType || 'detail');
}
@Get(':id')

View File

@@ -1,6 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { TenantDatabaseService } from '../tenant/tenant-database.service';
import { CreatePageLayoutDto, UpdatePageLayoutDto } from './dto/page-layout.dto';
import { CreatePageLayoutDto, UpdatePageLayoutDto, PageLayoutType } from './dto/page-layout.dto';
@Injectable()
export class PageLayoutService {
@@ -8,17 +8,19 @@ export class PageLayoutService {
async create(tenantId: string, createDto: CreatePageLayoutDto) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const layoutType = createDto.layoutType || 'detail';
// If this layout is set as default, unset other defaults for the same object
// If this layout is set as default, unset other defaults for the same object and layout type
if (createDto.isDefault) {
await knex('page_layouts')
.where({ object_id: createDto.objectId })
.where({ object_id: createDto.objectId, layout_type: layoutType })
.update({ is_default: false });
}
const [id] = await knex('page_layouts').insert({
name: createDto.name,
object_id: createDto.objectId,
layout_type: layoutType,
is_default: createDto.isDefault || false,
layout_config: JSON.stringify(createDto.layoutConfig),
description: createDto.description || null,
@@ -29,7 +31,7 @@ export class PageLayoutService {
return result;
}
async findAll(tenantId: string, objectId?: string) {
async findAll(tenantId: string, objectId?: string, layoutType?: PageLayoutType) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
let query = knex('page_layouts');
@@ -38,6 +40,10 @@ export class PageLayoutService {
query = query.where({ object_id: objectId });
}
if (layoutType) {
query = query.where({ layout_type: layoutType });
}
const layouts = await query.orderByRaw('is_default DESC, name ASC');
return layouts;
}
@@ -54,11 +60,11 @@ export class PageLayoutService {
return layout;
}
async findDefaultByObject(tenantId: string, objectId: string) {
async findDefaultByObject(tenantId: string, objectId: string, layoutType: PageLayoutType = 'detail') {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const layout = await knex('page_layouts')
.where({ object_id: objectId, is_default: true })
.where({ object_id: objectId, is_default: true, layout_type: layoutType })
.first();
return layout || null;
@@ -68,13 +74,12 @@ export class PageLayoutService {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
// Check if layout exists
await this.findOne(tenantId, id);
const layout = await this.findOne(tenantId, id);
// If setting as default, unset other defaults for the same object
// If setting as default, unset other defaults for the same object and layout type
if (updateDto.isDefault) {
const layout = await this.findOne(tenantId, id);
await knex('page_layouts')
.where({ object_id: layout.object_id })
.where({ object_id: layout.object_id, layout_type: layout.layout_type })
.whereNot({ id })
.update({ is_default: false });
}