Use AI assistant to create records in the system, added configurable list views
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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>;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user