Vyuh CDX

Entity Configuration

Complete guide to EntityConfiguration class and bringing all entity aspects together

Entity Configuration - Complete Guide

The EntityConfiguration class is the heart of the Vyuh Entity System. It brings together all aspects of an entity - from metadata and API to layouts and forms - into a single, cohesive configuration. This guide provides an in-depth look at every aspect of entity configuration.

Overview

class EntityConfiguration<T extends EntityBase> {
  final EntityMetadata metadata;
  final EntityApi<T> Function(SharedApiClient) api;
  final EntityLayoutDescriptor<T> layouts;
  final EntityFormDescriptor<T> form;
  final EntityActions<T>? actions;
  final RouteBuilder? routeBuilder;
  
  // ... constructor and methods
}

Core Components

1. EntityMetadata

The metadata describes your entity and controls how it appears throughout the system.

class EntityMetadata {
  final String identifier;        // Unique identifier (e.g., 'users', 'products')
  final String name;              // Singular display name
  final String pluralName;        // Plural display name
  final String? description;      // Entity description
  final IconData? icon;           // Display icon
  final Color? themeColor;        // Theme color for UI elements
  final String? category;         // Menu category
  final EntityRouteBuilder route; // Route configuration
  final Future<String?> Function(BuildContext)? getHelp; // Help provider
}

Example Configuration

EntityMetadata(
  identifier: 'reference_standards',
  name: 'Reference Standard',
  pluralName: 'Reference Standards',
  description: 'Chemical and biological reference materials',
  icon: Icons.science,
  themeColor: Colors.purple,
  category: 'Laboratory',
  route: EntityRouteBuilder.fromIdentifier('reference-standards'),
  getHelp: (context) async {
    return '''
    Reference standards are certified materials used for:
    - Instrument calibration
    - Method validation
    - Quality control
    ''';
  },
)

2. API Configuration

The API configuration provides a factory function that creates an EntityApi instance:

api: (client) => ReferenceStandardApi(client: client),

Your API class should extend EntityApi<T>:

class ReferenceStandardApi extends EntityApi<ReferenceStandard> {
  ReferenceStandardApi({required super.client});

  @override
  ReferenceStandard fromJson(Map<String, dynamic> json) => 
      ReferenceStandard.fromJson(json);

  // Optional: Override base methods for custom behavior
  @override
  Future<List<ReferenceStandard>> performList({
    int? offset,
    int? limit,
    String? sortBy,
    String? sortOrder,
    String? search,
  }) async {
    // Custom list implementation
    final response = await client.get(
      '/api/v1/reference-standards',
      queryParameters: {
        if (offset != null) 'offset': offset.toString(),
        if (limit != null) 'limit': limit.toString(),
        if (sortBy != null) 'sortBy': sortBy,
        if (sortOrder != null) 'sortOrder': sortOrder,
        if (search != null) 'search': search,
      },
    );
    
    final List<dynamic> data = response.data['data'];
    return data.map((json) => fromJson(json)).toList();
  }
}

3. Layout Configuration

The EntityLayoutDescriptor defines how your entity is displayed in different contexts:

class EntityLayoutDescriptor<T> {
  final List<EntityLayout<T>> list;         // List view layouts
  final List<EntityLayout<T>> details;      // Detail view layouts
  final List<EntityLayout<T>>? summary;     // Summary/card layouts
  final List<EntityLayout<T>>? dashboard;   // Dashboard view layouts
  final List<EntityLayout<T>>? analytics;   // Analytics contributions
}

List Layouts

list: [
  // Table layout for desktop
  TableListLayout<User>(
    title: 'Users',
    columns: [
      TableColumn(
        key: 'name',
        label: 'Name',
        getValue: (user) => user.name,
        sortable: true,
        searchable: true,
      ),
      TableColumn(
        key: 'email',
        label: 'Email',
        getValue: (user) => user.email,
        sortable: true,
      ),
      TableColumn(
        key: 'role',
        label: 'Role',
        getValue: (user) => user.role.toUpperCase(),
        width: 100,
      ),
      TableColumn(
        key: 'status',
        label: 'Status',
        getValue: (user) => user.isActive ? 'Active' : 'Inactive',
        buildCell: (context, user) => Chip(
          label: Text(user.isActive ? 'Active' : 'Inactive'),
          backgroundColor: user.isActive ? Colors.green : Colors.grey,
        ),
      ),
    ],
    onRowTap: (context, user) {
      // Navigate to detail view
      context.go('/users/${user.id}');
    },
    bulkActions: [
      BulkAction(
        label: 'Activate',
        icon: Icons.check,
        onTap: (context, users) async {
          // Bulk activate logic
        },
      ),
    ],
  ),
  
  // Grid layout for mobile/tablet
  GridListLayout<User>(
    title: 'Users',
    crossAxisCount: 2,
    buildCard: (context, user) => Card(
      child: InkWell(
        onTap: () => context.go('/users/${user.id}'),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              CircleAvatar(
                child: Text(user.name[0]),
              ),
              const SizedBox(height: 8),
              Text(
                user.name,
                style: Theme.of(context).textTheme.titleMedium,
              ),
              Text(
                user.email,
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],
          ),
        ),
      ),
    ),
  ),
]

Detail Layouts

details: [
  // Standard detail layout
  EntityLayoutConfiguration<User>(
    canHandle: (context) => true, // Always use this layout
    build: (context, user) => SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Header section
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  CircleAvatar(
                    radius: 40,
                    child: Text(user.name[0]),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          user.name,
                          style: Theme.of(context).textTheme.headlineMedium,
                        ),
                        Text(user.email),
                        Chip(
                          label: Text(user.role),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
          
          // Information sections
          const SizedBox(height: 24),
          _buildSection(
            context,
            title: 'Contact Information',
            children: [
              _buildInfoRow('Phone', user.phone),
              _buildInfoRow('Department', user.department),
              _buildInfoRow('Location', user.location),
            ],
          ),
          
          // Related entities
          const SizedBox(height: 24),
          _buildSection(
            context,
            title: 'Permissions',
            children: [
              FutureBuilder<List<Permission>>(
                future: _loadUserPermissions(user.id),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) {
                    return const CircularProgressIndicator();
                  }
                  return Wrap(
                    spacing: 8,
                    children: snapshot.data!.map((permission) {
                      return Chip(label: Text(permission.name));
                    }).toList(),
                  );
                },
              ),
            ],
          ),
        ],
      ),
    ),
  ),
]

4. Form Configuration

The EntityFormDescriptor handles form creation and data transformation:

abstract class EntityFormDescriptor<T> {
  StepForm prepare(T? entity);
  Map<String, dynamic> toFormData(T entity);
  T fromFormData(Map<String, dynamic> data, {T? entity});
}

Complex Form Example

class UserFormDescriptor extends EntityFormDescriptor<User> {
  @override
  StepForm prepare(User? entity) {
    final isEdit = entity != null;
    
    return StepForm(
      title: isEdit ? 'Edit User' : 'Create User',
      steps: [
        FormStep(
          title: 'Basic Information',
          form: FormBuilder(
            title: 'User Details',
            fields: [
              TextField(
                name: 'name',
                label: 'Full Name',
                value: entity?.name,
                validators: [
                  required(),
                  minLength(3),
                  maxLength(100),
                ],
              ),
              TextField(
                name: 'email',
                label: 'Email Address',
                value: entity?.email,
                validators: [
                  required(),
                  email(),
                  // Custom async validator
                  AsyncValidator(
                    validator: (value) async {
                      if (isEdit && value == entity.email) return null;
                      final exists = await checkEmailExists(value);
                      return exists ? 'Email already in use' : null;
                    },
                  ),
                ],
              ),
              SelectField(
                name: 'role',
                label: 'Role',
                value: entity?.role,
                options: [
                  Option(value: 'admin', label: 'Administrator'),
                  Option(value: 'manager', label: 'Manager'),
                  Option(value: 'user', label: 'Standard User'),
                ],
                validators: [required()],
              ),
            ],
          ),
        ),
        FormStep(
          title: 'Department & Location',
          form: FormBuilder(
            title: 'Assignment',
            fields: [
              SelectField(
                name: 'department',
                label: 'Department',
                value: entity?.department,
                optionsBuilder: () async {
                  final departments = await loadDepartments();
                  return departments.map((d) => 
                    Option(value: d.id, label: d.name)
                  ).toList();
                },
                validators: [required()],
              ),
              SelectField(
                name: 'location',
                label: 'Location',
                value: entity?.location,
                dependsOn: ['department'],
                optionsBuilder: (formData) async {
                  final deptId = formData['department'];
                  if (deptId == null) return [];
                  
                  final locations = await loadLocations(deptId);
                  return locations.map((l) => 
                    Option(value: l.id, label: l.name)
                  ).toList();
                },
              ),
              TextField(
                name: 'phone',
                label: 'Phone Number',
                value: entity?.phone,
                validators: [
                  pattern(r'^\+?[\d\s-()]+$', 'Invalid phone number'),
                ],
              ),
            ],
          ),
        ),
        FormStep(
          title: 'Permissions',
          form: FormBuilder(
            title: 'Access Control',
            fields: [
              MultiSelectField(
                name: 'permissions',
                label: 'Permissions',
                value: entity?.permissions,
                optionsBuilder: () async {
                  final permissions = await loadAvailablePermissions();
                  return permissions.map((p) => 
                    Option(
                      value: p.id,
                      label: p.name,
                      description: p.description,
                    )
                  ).toList();
                },
              ),
              ToggleField(
                name: 'isActive',
                label: 'Active Account',
                value: entity?.isActive ?? true,
                description: 'Inactive accounts cannot log in',
              ),
            ],
          ),
        ),
      ],
    );
  }

  @override
  Map<String, dynamic> toFormData(User entity) {
    return {
      'name': entity.name,
      'email': entity.email,
      'role': entity.role,
      'department': entity.department,
      'location': entity.location,
      'phone': entity.phone,
      'permissions': entity.permissions,
      'isActive': entity.isActive,
    };
  }

  @override
  User fromFormData(Map<String, dynamic> data, {User? entity}) {
    return User(
      id: entity?.id ?? '',
      schemaType: 'users',
      name: data['name'],
      email: data['email'],
      role: data['role'],
      department: data['department'],
      location: data['location'],
      phone: data['phone'],
      permissions: List<String>.from(data['permissions'] ?? []),
      isActive: data['isActive'],
      createdAt: entity?.createdAt ?? DateTime.now(),
    );
  }
}

5. Entity Actions

Actions provide custom operations for single entities or bulk operations:

class EntityActions<T> {
  final List<EntityAction<T>>? list;    // Actions for list view
  final List<EntityAction<T>>? single;  // Actions for single entity
  final List<EntityAction<T>>? bulk;    // Bulk actions
}

class EntityAction<T> {
  final String label;
  final IconData? icon;
  final Future<void> Function(BuildContext, List<T>) onTap;
  final bool Function(List<T>)? isEnabled;
  final bool requiresConfirmation;
}

Action Examples

actions: EntityActions<Equipment>(
  list: [
    EntityAction(
      label: 'Import CSV',
      icon: Icons.upload_file,
      onTap: (context, _) async {
        final file = await pickFile();
        if (file != null) {
          await importEquipmentFromCsv(file);
          if (context.mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Import completed')),
            );
          }
        }
      },
    ),
  ],
  single: [
    EntityAction(
      label: 'Generate QR Code',
      icon: Icons.qr_code,
      onTap: (context, entities) async {
        final equipment = entities.first;
        final qrCode = await generateQrCode(equipment);
        if (context.mounted) {
          showDialog(
            context: context,
            builder: (_) => QrCodeDialog(qrCode: qrCode),
          );
        }
      },
    ),
    EntityAction(
      label: 'Schedule Maintenance',
      icon: Icons.calendar_today,
      isEnabled: (entities) => 
        entities.first.status == EquipmentStatus.operational,
      onTap: (context, entities) async {
        final equipment = entities.first;
        await showMaintenanceScheduler(context, equipment);
      },
    ),
  ],
  bulk: [
    EntityAction(
      label: 'Assign to Location',
      icon: Icons.location_on,
      requiresConfirmation: true,
      onTap: (context, entities) async {
        final location = await showLocationPicker(context);
        if (location != null) {
          await assignEquipmentToLocation(entities, location);
        }
      },
    ),
  ],
),

6. Custom Route Builder

For advanced routing needs, provide a custom route builder:

routeBuilder: (configuration) {
  return (BuildContext context, GoRouterState state) {
    // Custom route handling
    final entityId = state.pathParameters['id'];
    
    // Example: Add animation
    return CustomTransitionPage(
      child: EntityDetailView<Equipment>(
        configuration: configuration,
        entityId: entityId!,
      ),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return FadeTransition(
          opacity: animation,
          child: child,
        );
      },
    );
  };
},

Complete Configuration Example

Here's a comprehensive example bringing all components together:

class LocationConfig {
  static final instance = EntityConfiguration<Location>(
    metadata: EntityMetadata(
      identifier: 'locations',
      name: 'Location',
      pluralName: 'Locations',
      description: 'Physical locations and facilities',
      icon: Icons.location_city,
      themeColor: Colors.teal,
      category: 'Organization',
      route: EntityRouteBuilder.fromIdentifier('locations'),
      getHelp: (context) async => 
        await loadHelpContent('locations'),
    ),
    
    api: (client) => LocationApi(client: client),
    
    layouts: EntityLayoutDescriptor(
      list: [
        TableListLayout<Location>(
          title: 'Locations',
          columns: _buildTableColumns(),
          filters: _buildFilters(),
          defaultSort: 'name',
        ),
        LocationMapLayout(), // Custom map view
      ],
      details: [
        LocationDetailLayout(),
        LocationFloorPlanLayout(), // Custom floor plan view
      ],
      summary: [
        LocationCardLayout(),
      ],
      dashboard: [
        LocationStatsWidget(),
        LocationUtilizationChart(),
      ],
    ),
    
    form: LocationFormDescriptor(),
    
    actions: EntityActions<Location>(
      list: [
        EntityAction(
          label: 'Import Locations',
          icon: Icons.upload,
          onTap: _handleImport,
        ),
      ],
      single: [
        EntityAction(
          label: 'View Floor Plan',
          icon: Icons.map,
          onTap: _showFloorPlan,
        ),
        EntityAction(
          label: 'Generate Report',
          icon: Icons.description,
          onTap: _generateReport,
        ),
      ],
    ),
  );
}

Advanced Configuration Patterns

Dynamic Configuration

Configuration can be dynamic based on user permissions or context:

EntityConfiguration<Document>(
  // ... other config
  layouts: EntityLayoutDescriptor(
    list: [
      // Show different layouts based on user role
      if (userRole == 'admin') 
        AdminDocumentListLayout()
      else
        StandardDocumentListLayout(),
    ],
  ),
  actions: EntityActions(
    single: [
      // Conditionally add actions
      if (hasPermission('documents.approve'))
        EntityAction(
          label: 'Approve',
          icon: Icons.check,
          onTap: _approveDocument,
        ),
    ],
  ),
)

Configuration Composition

Build configurations from reusable components:

class BaseEntityConfig {
  static EntityLayoutDescriptor<T> createStandardLayouts<T>() {
    return EntityLayoutDescriptor<T>(
      list: [
        TableListLayout<T>(...),
        GridListLayout<T>(...),
      ],
      details: [
        StandardDetailLayout<T>(),
      ],
    );
  }
  
  static EntityActions<T> createStandardActions<T>() {
    return EntityActions<T>(
      list: [
        EntityAction(
          label: 'Export',
          icon: Icons.download,
          onTap: (context, entities) => exportEntities(entities),
        ),
      ],
    );
  }
}

// Use in specific configurations
final config = EntityConfiguration<Product>(
  // ... metadata and api
  layouts: BaseEntityConfig.createStandardLayouts<Product>(),
  actions: BaseEntityConfig.createStandardActions<Product>(),
  form: ProductFormDescriptor(),
);

Best Practices

  1. Keep configurations focused - Each configuration should handle one entity type
  2. Use type safety - Leverage Dart's type system for compile-time safety
  3. Provide multiple layouts - Different users prefer different views
  4. Include help content - Use the getHelp function for context-sensitive help
  5. Test configurations - Unit test form descriptors and API implementations
  6. Document custom behaviors - Comment any non-standard implementations

Next: Metadata API - Deep dive into entity metadata and API patterns