Skip to content

simtlix/simfinity-fe

Repository files navigation

Simfinity Frontend

A Next.js 15.4.6 frontend application for Simfinity, featuring a dynamic, schema-driven form management system built with GraphQL, urql, and Material-UI v7.3.1.

License: Apache 2.0

Overview

This application automatically generates forms from GraphQL schema introspection and supports complex entity management including embedded objects, collections, and extensive customization capabilities.

The system provides two main components:

  • EntityTable: Dynamic data tables with sorting, filtering, and pagination
  • EntityForm: Schema-driven forms for creating, editing, and viewing entities

Features

πŸš€ Core Capabilities

  • Dynamic Form Generation: Automatically creates forms from GraphQL schema
  • Schema Introspection: Real-time field discovery and type detection
  • Collection Management: Handle complex nested collections with add/edit/delete
  • Embedded Objects: Support for nested object structures
  • Internationalization: Multi-language support (English/Spanish)
  • Form Customization: Field-level visibility, validation, and layout control
  • State Machine Support: Entity state transitions with business rule validation
  • GraphQL Integration: Native urql support with dynamic queries

🎯 Entity Management

  • Create Mode: Build new entities with collections from scratch
  • Edit Mode: Modify existing entities with change tracking
  • View Mode: Read-only display of entity data
  • Collection Support: Manage related entities (e.g., episodes in a series)
  • Validation: Built-in form validation with custom rules

🎨 UI/UX Features

  • Material-UI v7: Modern, responsive design system
  • Responsive Layout: Mobile-first approach with grid system
  • Custom Field Renderers: Specialized input components for different data types
  • Accordion Sections: Organized embedded object display
  • Real-time Updates: urql cache management with graphcache

Getting Started

First, run the development server:

npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev

Open http://localhost:3000 with your browser to see the result.

Components

πŸ“Š EntityTable

The EntityTable component provides dynamic data tables with automatic column generation based on GraphQL schema introspection.

Features

  • Automatic Column Detection: Columns generated from schema fields
  • Sorting: Server-side sorting with GraphQL integration
  • Pagination: Server-side pagination with configurable page sizes
  • Filtering: Advanced filtering capabilities
  • Custom Column Renderers: Specialized display for different data types
  • Responsive Design: Mobile-friendly table layout

Usage

import EntityTable from '@/components/EntityTable';

// Basic usage
<EntityTable 
  listField="series" 
  entityTypeName="Serie"
/>

// With customization
<EntityTable 
  listField="episodes"
  entityTypeName="Episode"
  customizationState={customState}
  onCustomizationChange={handleCustomization}
/>

Props

  • listField: The plural field name (e.g., "series", "episodes")
  • entityTypeName: The singular entity type name (e.g., "Serie", "Episode")
  • customizationState: Form customization state for field behavior
  • onCustomizationChange: Callback for customization updates

Example Output

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Serie Name    β”‚ Director    β”‚ Year β”‚ Episodes β”‚ Actions   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Breaking Bad  β”‚ Vince G.    β”‚ 2008 β”‚ 62       β”‚ [Edit] [Delete] β”‚
β”‚ Game of Thronesβ”‚ David B.   β”‚ 2011 β”‚ 73       β”‚ [Edit] [Delete] β”‚
β”‚ Stranger Thingsβ”‚ Duffer Brosβ”‚ 2016 β”‚ 34       β”‚ [Edit] [Delete] β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“ EntityForm

The EntityForm component automatically generates forms from GraphQL schema introspection, supporting create, edit, and view modes with embedded objects and collections.

Features

  • Dynamic Form Generation: Forms built automatically from schema
  • Three Modes: Create, Edit, and View with appropriate behaviors
  • Embedded Objects: Nested object support with accordion sections
  • Collection Management: Add/edit/delete related entities
  • Field Validation: Built-in validation with custom rules
  • Internationalization: Multi-language field labels and messages
  • Responsive Layout: Material-UI grid system with customization

Usage

import EntityForm from '@/components/EntityForm';

// Create new entity
<EntityForm 
  listField="series" 
  action="create"
/>

// Edit existing entity
<EntityForm 
  listField="series" 
  entityId="123"
  action="edit"
/>

// View entity (read-only)
<EntityForm 
  listField="series" 
  entityId="123"
  action="view"
/>

Props

  • listField: The plural field name (e.g., "series")
  • entityId: Entity ID for edit/view modes (undefined for create)
  • action: Form mode ("create" | "edit" | "view")

Form Modes

Create Mode
  • Purpose: Build new entities from scratch
  • Collections: Add items to empty collections
  • Validation: Required field validation
  • Submission: Creates new entity with collections
Edit Mode
  • Purpose: Modify existing entities
  • Collections: Add/modify/delete collection items
  • Change Tracking: Tracks all modifications
  • Submission: Updates existing entity with changes
View Mode
  • Purpose: Read-only display of entity data
  • Collections: Display collection items
  • No Editing: All fields are disabled
  • Navigation: Links to edit mode

Field Types Supported

Scalar Fields
  • Text: String inputs with validation
  • Numbers: Numeric inputs with type checking
  • Booleans: Checkbox inputs
  • Dates: Date picker inputs
  • Enums: Dropdown selections
  • Lists: Tag-based input for arrays
Object Fields
  • References: Foreign key relationships
  • Search: Object selector with search
  • Display: Custom field rendering
Embedded Objects
  • Nested Fields: Fields within objects
  • Accordion Sections: Collapsible sections
  • Validation: Individual field validation
  • Layout: Customizable field sizing
Collection Fields
  • Grid Display: Data grid for items
  • Add/Edit/Delete: Full CRUD operations
  • Change Tracking: Local state management
  • Bulk Operations: Multiple item management

Example Form Structure

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Create Serie                                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Name:        [Breaking Bad                    ]            β”‚
β”‚ Description: [A chemistry teacher turns...    ]            β”‚
β”‚ Year:        [2008                            ]            β”‚
β”‚ Categories:  [Crime] [Drama] [Thriller] [+Add]            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Director (Embedded Object)                    [β–Ό]         β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Name:    [Vince Gilligan                ]              β”‚ β”‚
β”‚ β”‚ Country: [United States                 ]              β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Episodes (Collection)                        [β–Ό]          β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ [Add Episode]                                          β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Number: [1] Name: [Pilot] Date: [2008-01-20]      β”‚ β”‚
β”‚ β”‚ β”‚ [Edit] [Remove]                                    β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ [Cancel]                                    [Create]      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Customization

🎨 Form Customization System

The system provides extensive customization capabilities for form behavior and appearance through the @simtlix/simfinity-fe-components package.

Setup Configuration

All customizations are registered in src/examples/setupConfiguration.ts:

import { setupEpisodeFormCustomization } from "./episodeFormCustomization";
import { setupSerieFormCustomization } from "./serieFormCustomization";
import { setupI18n } from "./i18nSetup";
import { setupColumnRenderers } from "./columnRenderersSetup";
import { setupSeasonStateMachine } from "./seasonStateMachineExample";

export const setupConfigurations = () => {
  // Setup i18n labels
  setupI18n();
  
  // Setup custom column renderers
  setupColumnRenderers();
  
  // Setup form customizations
  setupEpisodeFormCustomization();
  setupSerieFormCustomization();
  
  // Setup state machines
  setupSeasonStateMachine();
};

Form Customization Examples

Basic Field Customization
import { registerFormCustomization } from '@simtlix/simfinity-fe-components';

export function setupEpisodeFormCustomization() {
  registerFormCustomization("episode", "create", {
    fieldsCustomization: {
      name: {
        size: { xs: 12, sm: 6, md: 6 }, // Half width
        order: 1,
        onChange: (fieldName, value, formData, setFieldData, setFieldVisible, setFieldEnabled, parentFormAccess) => {
          // Custom logic: enable other fields when name has value
          if (value && String(value).trim() !== '') {
            setFieldEnabled('number', true);
            setFieldEnabled('season', true);
            setFieldData('number', 1);
          } else {
            setFieldEnabled('number', false);
            setFieldEnabled('season', false);
            setFieldData('number', "");
            setFieldData('season', "");
          }
          
          // Note: parentFormAccess is undefined for main entity fields
          // It's only available in collection item context
          return { value, error: undefined };
        }
      },
      number: {
        size: { xs: 12, sm: 6, md: 6 },
        order: 2,
        // Dynamic enabled state based on form data
        enabled: (fieldName, value, formData) => {
          const nameValue = formData.name?.value;
          return !!(nameValue && String(nameValue).trim() !== '');
        }
      }
    }
  });
}
Custom Renderer for Fields
export function setupSerieFormCustomization() {
  registerFormCustomization("serie", "create", {
    fieldsCustomization: {
      description: {
        size: { xs: 12 },
        order: 3,
        customRenderer: (field, customizationActions, handleFieldChange, disabled) => {
          return (
            <RichTextEditor
              value={field.value as string || ''}
              onChange={(value) => handleFieldChange(field.name, value)}
              disabled={disabled}
              error={field.error}
            />
          );
        }
      }
    }
  });
}
Custom Embedded Renderer
export function setupSerieFormCustomization() {
  registerFormCustomization("serie", "create", {
    fieldsCustomization: {
      // Embedded section customization
      director: {
        size: { xs: 12, sm: 6, md: 6 }, // Section size
        order: 4,
        customEmbeddedRenderer: (
          field,
          customizationActions,
          handleEmbeddedFieldChange,
          disabled,
          formData
        ) => {
          const nameField = field.embeddedFields?.find(f => f.name.endsWith('.name'));
          const countryField = field.embeddedFields?.find(f => f.name.endsWith('.country'));
  
  return (
            <Paper elevation={2} sx={{ p: 3 }}>
              <Typography variant="h6" sx={{ mb: 2 }}>🎬 Director Information</Typography>
              <TextField
                label="Director Name"
                value={(formData[nameField?.name || ''] as { value?: string })?.value || ''}
                onChange={(e) => handleEmbeddedFieldChange(field.name, 'name', e.target.value)}
                disabled={disabled}
                fullWidth
              />
              <CountrySelector
                value={(formData[countryField?.name || ''] as { value?: string })?.value || ''}
                onChange={(value) => handleEmbeddedFieldChange(field.name, 'country', value)}
                disabled={disabled}
              />
            </Paper>
          );
        }
      }
    }
  });
}
Custom Collection Renderer
export function setupCastCustomization() {
  registerFormCustomization("serie", "edit", {
    fieldsCustomization: {
      cast: {
        size: { xs: 12 },
        order: 5,
        customCollectionRenderer: (
          collectionFieldName,
          parentFormAccess,
          collectionState,
          onCollectionStateChange,
          parentEntityId,
          isEditMode
        ) => {
          const allItems = [
            ...(collectionState.added || []),
            ...(collectionState.modified || []),
            ...(collectionState.original || [])
          ].filter(item => item.__status !== 'deleted');
          
          const handleAddActor = () => {
            const newActor = {
              id: `temp-${Date.now()}`,
              name: 'New Actor',
              role: 'Character',
              __status: 'added'
            };
            
            onCollectionStateChange({
              ...collectionState,
              added: [...(collectionState.added || []), newActor]
            });
          };
  
  return (
            <Box>
              <Typography variant="h6">Cast</Typography>
              <List>
                {allItems.map((actor) => (
                  <ListItem key={actor.id}>
                    <ListItemText primary={actor.name} secondary={actor.role} />
                    <IconButton onClick={() => handleRemoveActor(actor)}>
                      <DeleteIcon />
                    </IconButton>
                  </ListItem>
                ))}
              </List>
              <Button onClick={handleAddActor}>Add Cast Member</Button>
    </Box>
  );
        }
      }
    }
  });
}
Entity-Level Callbacks (beforeSubmit, onSuccess, onError)
export function setupEpisodeCallbacks() {
  registerFormCustomization("episode", "create", {
    fieldsCustomization: {},
    beforeSubmit: async (formData, collectionChanges, transformedData, actions) => {
      // Validate episode number
      const episodeNumber = formData.number?.value;
      if (episodeNumber === 1) {
        actions.setFormMessage({
          type: 'warning',
          message: 'Episode 1 is typically the pilot. Please verify this is correct.'
        });
      }
      
      // Auto-generate description if empty
      if (!formData.description?.value) {
        const name = formData.name?.value;
        if (name) {
          actions.setFieldData('description', `Episode ${episodeNumber}: ${name}`);
        }
      }
      
      // Validate collection changes
      if (collectionChanges.guestStars) {
        const { added, modified } = collectionChanges.guestStars;
        if (added.length + modified.length > 5) {
          actions.setFormMessage({
            type: 'warning',
            message: 'You have many guest stars. Consider if this is a special episode.'
          });
        }
      }
      
      // Return true to continue, false to cancel submission
      return true;
    },
    onSuccess: async (result) => {
      console.log('Episode created successfully:', result);
      return {
        message: 'Episode created successfully! Would you like to add another?',
        navigateTo: undefined, // Stay on current page
        action: () => console.log('Custom success action')
      };
    },
    onError: async (error, formData, actions) => {
      console.error('Error creating episode:', error);
      
      if (error.message.includes('duplicate')) {
        actions.setFormMessage({
          type: 'error',
          message: 'An episode with this number already exists in this season.'
        });
        actions.setFieldData('number', '');
      } else {
        actions.setFormMessage({
          type: 'error',
          message: `Failed to create episode: ${error.message}`
        });
      }
    }
  });
}
Collection Field Customization
export function setupSeasonCollectionCustomization() {
  registerFormCustomization("serie", "edit", {
    fieldsCustomization: {
      seasons: {
        size: { xs: 12 },
        order: 2,
        
        // Delete callback - executed when delete button is pressed
        onDelete: async (item, setMessage) => {
          if (item.episodeCount > 0) {
            setMessage({
              type: 'error',
              message: `Cannot delete season "${item.name}" - it has episodes`
            });
            return false; // Cancel deletion
          }
          return true; // Allow deletion
        },
        
        // Edit mode customization for collection items
        onEdit: {
          fieldsCustomization: {
            name: {
              size: { xs: 12, sm: 6 },
              order: 1,
              onChange: (fieldName, value, formData, setFieldData, setFieldVisible, setFieldEnabled, parentFormAccess) => {
                // Can access parent form data
                if (parentFormAccess) {
                  const seriesTitle = parentFormAccess.parentFormData.title?.value;
                  console.log('Editing season for series:', seriesTitle);
                }
                return { value, error: undefined };
              }
            }
          },
          onSubmit: async (item, setFieldData, formData, setFieldVisible, setFieldEnabled, setMessage, parentFormAccess) => {
            // Validate before saving
            if (!item.name?.trim()) {
              setMessage({
            type: 'error',
                message: 'Season name is required'
              });
              return false;
            }
            
            // Auto-generate slug
            if (!item.slug) {
              const slug = String(item.name).toLowerCase().replace(/\s+/g, '-');
              setFieldData('slug', slug);
            }
            
            return true;
          }
        },
        
        // Create mode customization for collection items
        onCreate: {
          fieldsCustomization: {
            number: {
              size: { xs: 12, sm: 6 },
              order: 1
            }
          },
          onSubmit: async (item, setFieldData, formData, setFieldVisible, setFieldEnabled, setMessage, parentFormAccess) => {
            // Auto-increment season number
            if (!item.number) {
              const nextNumber = getNextSeasonNumber();
              setFieldData('number', nextNumber);
            }
            return true;
          }
        }
      }
    }
  });
}

πŸ”„ State Machine System

State machines allow you to manage entity state transitions with custom validation and business logic.

State Machine Registration

import { registerEntityStateMachine } from '@simtlix/simfinity-fe-components';

export function setupSeasonStateMachine() {
  registerEntityStateMachine("season", {
    actions: {
      activate: {
        mutation: 'activate_season',
        from: 'SCHEDULED',
        to: 'ACTIVE',
        onBeforeSubmit: async (formData, collectionChanges, transformedData, actions) => {
          console.log('Before activating season:', { formData, collectionChanges, transformedData });
          
          // Validate business rules before transition
          const episodesChanges = collectionChanges.episodes || { added: [], modified: [], deleted: [] };
          const newEpisodesCount = episodesChanges.added.length;
          
          if (newEpisodesCount === 0) {
            actions.setFormMessage({
              type: 'error',
              message: 'Cannot activate season without episodes'
            });
            return { shouldProceed: false, error: 'Season must have at least one episode' };
          }
          
          return { shouldProceed: true };
        },
        onSuccess: async (result, formData, collectionChanges, transformedData, actions) => {
          console.log('Season activated successfully:', result);
          
          actions.setFormMessage({
            type: 'success',
            message: 'Season activated successfully!'
          });
        },
        onError: async (error, formData, collectionChanges, transformedData, actions) => {
          console.error('Failed to activate season:', error);
          
          actions.setFormMessage({
            type: 'error',
            message: `Failed to activate season: ${error.message}`
          });
        }
      },
      finalize: {
        mutation: 'finalize_season',
        from: 'ACTIVE',
        to: 'FINISHED',
        onBeforeSubmit: async (formData, collectionChanges, transformedData, actions) => {
          // Query server for validation
          const GET_EPISODES = gql`
            query GetEpisodes($seasonId: QLValue!) {
              episodes(season: { terms: { path: "id", operator: EQ, value: $seasonId } }) {
                id
                date
              }
            }
          `;
          
          const result = await urqlClient.query(GET_EPISODES, {
            seasonId: transformedData.id
          }).toPromise();
          const { data } = result;
          });
          
          const existingEpisodes = data?.episodes || [];
          const incompleteEpisodes = existingEpisodes.filter(ep => 
            !ep.date || new Date(ep.date) > new Date()
          );
          
          if (incompleteEpisodes.length > 0) {
            actions.setFormMessage({
              type: 'error',
              message: 'Cannot finalize season with incomplete episodes'
            });
            return { shouldProceed: false, error: 'All episodes must be completed' };
          }
          
          return { shouldProceed: true };
        },
        onSuccess: async (result, formData, collectionChanges, transformedData, actions) => {
          actions.setFormMessage({
            type: 'success',
            message: 'Season finalized successfully!'
          });
        },
        onError: async (error, formData, collectionChanges, transformedData, actions) => {
          actions.setFormMessage({
            type: 'error',
            message: `Failed to finalize season: ${error.message}`
          });
        }
      }
    }
  });
}

State Machine Callbacks

onBeforeSubmit Parameters:

  • formData - Current form field values
  • collectionChanges - Changes to collection fields (added, modified, deleted)
  • transformedData - Processed entity data ready for mutation
  • actions - Actions to manipulate form state

onBeforeSubmit Return Value:

  • { shouldProceed: true } - Allow state transition
  • { shouldProceed: false, error: string } - Cancel state transition

onSuccess/onError Parameters:

  • result - GraphQL mutation result
  • formData - Current form field values
  • collectionChanges - Collection changes
  • transformedData - Processed entity data
  • actions - Actions to manipulate form state

State Machine Integration

The EntityForm automatically detects registered state machines and:

  1. Shows Actions Button - Displays "Actions" button in edit mode
  2. Dynamic Menu - Shows available actions based on current entity state
  3. Field Management - Excludes state machine fields from create forms
  4. Read-only Display - Shows state machine fields as read-only
  5. Form Refresh - Reloads entity data after successful transitions

State Machine i18n Labels

{
  "stateMachine.season.action.activate": "Activate",
  "stateMachine.season.action.finalize": "Finalize",
  "stateMachine.season.state.SCHEDULED": "Scheduled",
  "stateMachine.season.state.ACTIVE": "Active",
  "stateMachine.season.state.FINISHED": "Finished",
  "stateMachine.actions": "Actions"
}

πŸ“Š Column Customization System

Custom column renderers for specialized data display in EntityTable:

import { registerColumnRenderer } from "@simtlix/simfinity-fe-components";
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";

export const setupColumnRenderers = () => {
  // Custom date renderer with icon
  registerColumnRenderer("episode.date", ({ value }) => {
    if (value == null) return "";
    const d = new Date(value as string | number);
    const text = isNaN(d.getTime()) ? String(value) : d.toLocaleDateString();
    return (
      <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
        <CalendarMonthIcon fontSize="small" />
        {text}
      </span>
    );
  });

  // Custom renderer with context
  registerColumnRenderer("season.year", ({ value, rowData }) => {
    if (value == null) return "";
    return (
      <span style={{ fontWeight: "bold", color: "#1976d2" }}>
        Year {value}
      </span>
    );
  });
};

🌍 Internationalization (i18n) System

The project supports two types of internationalization:

1. Static JSON Files (public/i18n/)

Location: public/i18n/en.json, public/i18n/es.json Usage: Static labels for entities, fields, and common UI elements

{
  "entity.serie.single": "Serie",
  "entity.serie.plural": "Series",
  "serie.name": "Name",
  "serie.description": "Description",
  "form.create": "Create",
  "form.edit": "Edit",
  "form.view": "View",
  "grid.filter.contains": "contains",
  "grid.filter.equals": "equals"
}

2. Function-based Labels (src/i18n/)

Location: src/i18n/en.ts, src/i18n/es.ts Usage: Dynamic labels with context and custom column renderers

import { registerFunctionLabels, registerColumnRenderer } from "@simtlix/simfinity-fe-components";

export const labels: Record<string, LabelValue> = {
  // Dynamic labels with context
  "season.year": (ctx) => `Year (${ctx.entity})`,
  "serie.name": "Title", // Override JSON labels
};

// Register function-based labels
registerFunctionLabels("en", labels);

// Register custom column renderers
registerColumnRenderer("episode.date", ({ value }) => {
  if (value == null) return "";
  const d = new Date(value as string | number);
  const text = isNaN(d.getTime()) ? String(value) : d.toLocaleDateString();
  return React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 } },
    React.createElement(CalendarMonthIcon, { fontSize: "small" }),
    text
  );
});

Label Resolution Strategy

// Multi-level fallback system
const getFieldLabel = (fieldName: string): string => {
  const entityKey = listField.slice(0, -1); // Remove 's' from end
  const fieldKey = `${entityKey}.${fieldName}`;
  
  return resolveLabel([
    fieldKey,           // entity.field (e.g., "serie.name")
    fieldName,          // field name as fallback
  ], { entity: listField, field: fieldName }, fieldName);
};

Label Patterns

  • Entity Labels: entity.{entityType}.{form} (e.g., "serie.single", "serie.plural")
  • Field Labels: {entityType}.{fieldName} (e.g., "serie.name", "serie.description")
  • Form Labels: form.{action} (e.g., "form.create", "form.edit", "form.view")
  • Grid Labels: grid.filter.{type} (e.g., "grid.filter.contains", "grid.filter.equals")
  • State Machine Labels: stateMachine.{entity}.{type}.{value} (e.g., "stateMachine.season.state.ACTIVE")

Simfinity.js Compatibility

This frontend application is designed to work seamlessly with Simfinity.js, a Node.js framework that automatically generates GraphQL schemas, mutations, and queries. The components automatically introspect the GraphQL schema to understand entity structures and generate appropriate forms and tables.

πŸ”— Simfinity.js Integration

  • Automatic Schema Introspection: Components read GraphQL schema metadata to understand entity structures
  • Generated Mutations: All mutations follow Simfinity.js naming conventions (addserie, updateserie, deleteserie)
  • Generated Queries: Queries and filters are compatible with Simfinity.js generated endpoints
  • Metadata-Driven: Forms and tables are automatically generated based on schema extensions

πŸ“Š Schema Metadata Fields

Simfinity.js uses GraphQL schema extensions to define relationships and behavior:

1. displayField - Object Reference Display

// In Simfinity.js type definition
director: {
  type: new GraphQLNonNull(directorType),
  extensions: {
    relation: {
      displayField: 'name'  // Shows director name instead of ID
    }
  }
}

Usage in EntityForm/EntityTable:

  • ObjectFieldSelector: Uses displayField to show human-readable values
  • Table Display: Shows the displayField value instead of raw object data
  • Search: Users can search by the display field value

2. connectionField - Collection Relationships

// In Simfinity.js type definition
seasons: {
  type: new GraphQLList(seasonType),
  extensions: {
    relation: {
      connectionField: 'serie'  // Links seasons to parent serie
    }
  }
}

Usage in CollectionFieldGrid:

  • Parent Reference: Automatically sets the connection field value
  • Filtering: Only shows items related to the parent entity
  • Data Integrity: Ensures proper parent-child relationships

3. embedded: true - Embedded Objects

// In Simfinity.js type definition
director: {
  type: directorType,
  extensions: {
    relation: {
      embedded: true  // Treats as embedded object, not reference
    }
  }
}

Usage in EntityForm:

  • Inline Editing: Director fields appear directly in the serie form
  • No Separate Entity: Director data is stored within the serie record
  • Simplified UI: No need for object selection or separate forms

πŸ“Ί TV Series Management Example

Based on the Simfinity.js series-sample project, here's how the schema metadata works:

Simfinity.js Type Definition

// types/serie.js (from series-sample)
const serieType = new GraphQLObjectType({
  name: 'serie',
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: new GraphQLNonNull(GraphQLString) },
    categories: { type: new GraphQLList(GraphQLString) },
    description: { type: GraphQLString },
    director: {
      type: directorType,
      extensions: {
        relation: {
          embedded: true,
          displayField: 'name'
        }
      }
    },
    seasons: {
      type: new GraphQLList(seasonType),
      extensions: {
        relation: {
          connectionField: 'serie'
        }
      }
    }
  })
});

Generated GraphQL Schema

type Serie {
  id: ID!
  name: String!
  categories: [String]
  description: String
  director: Director  # Embedded object
  seasons: [Season]   # Collection with connectionField
}

type Director {
  name: String!
  country: String!
}

type Season {
  id: ID!
  number: Int!
  year: Int!
  serie: Serie!  # Back-reference to parent
}

Generated Mutations

# Create serie with embedded director and collection seasons
mutation AddSerie($input: serieInput!) {
  addserie(input: $input) {
    id
    name
    description
    director { name country }
    seasons { number year }
  }
}

Generated Queries

# List series with embedded and collection data
query Series {
  series {
    id
    name
    description
    director { name country }
    seasons { number year }
  }
}

πŸ”„ How Metadata Drives Component Behavior

EntityForm Behavior

  1. Schema Introspection: Reads GraphQL schema to discover fields and metadata
  2. Field Detection: Identifies embedded objects, collections, and references
  3. Form Generation: Automatically generates appropriate form controls
  4. Validation: Applies validation rules based on field types and metadata
  5. Mutation Generation: Creates proper Simfinity.js compatible mutations

EntityTable Behavior

  1. Column Generation: Creates columns based on schema fields and metadata
  2. Object Display: Uses displayField to show human-readable values
  3. Collection Handling: Manages collection fields with proper filtering
  4. Filtering: Generates Simfinity.js compatible filter queries
  5. Sorting: Supports sorting by object display fields

CollectionFieldGrid Behavior

  1. Connection Field: Automatically sets parent entity reference
  2. Local State Management: Tracks added, modified, and deleted items
  3. Schema-Driven: Uses metadata to understand collection structure
  4. Mutation Integration: Generates proper collection mutations for Simfinity.js

πŸ“ Usage Example

// In your GraphQL server (Simfinity.js)
const serieType = new GraphQLObjectType({
  name: 'serie',
  fields: () => ({
    director: {
      type: directorType,
      extensions: { relation: { embedded: true, displayField: 'name' } }
    },
    seasons: {
      type: new GraphQLList(seasonType),
      extensions: { relation: { connectionField: 'serie' } }
    }
  })
});

// Frontend automatically handles:
// - Embedded director editing in the same form
// - Season collection management with parent reference
// - Proper mutations for create/update/delete

πŸ”§ Customization Integration

Form Customization Setup

// src/examples/setupConfiguration.ts
import { setupEpisodeFormCustomization } from "./episodeFormCustomization";
import { setupSerieFormCustomization } from "./serieFormCustomization";
import { setupI18n } from "./i18nSetup";
import { setupColumnRenderers } from "./columnRenderersSetup";
import { setupSeasonStateMachine } from "./seasonStateMachineExample";

export const setupConfigurations = () => {
  setupI18n();
  setupColumnRenderers();
setupEpisodeFormCustomization();
  setupSerieFormCustomization();
  setupSeasonStateMachine();
};

Column Customization Setup

// src/examples/columnRenderersSetup.tsx
import { registerColumnRenderer } from "@simtlix/simfinity-fe-components";

export const setupColumnRenderers = () => {
registerColumnRenderer("episode.date", ({ value }) => {
    // Custom date rendering with icon
    return <DateWithIcon value={value} />;
  });
};

Architecture

πŸ“ Project Structure

src/
β”œβ”€β”€ app/                    # Next.js App Router pages
β”‚   β”œβ”€β”€ entities/          # Dynamic entity pages
β”‚   β”œβ”€β”€ layout.tsx         # Root layout
β”‚   └── providers.tsx      # App providers
β”œβ”€β”€ components/            # Reusable UI components
β”‚   └── app/              # App-specific components
β”œβ”€β”€ examples/             # Usage examples and patterns
β”‚   β”œβ”€β”€ columnRenderersSetup.tsx
β”‚   β”œβ”€β”€ episodeFormCustomization.ts
β”‚   β”œβ”€β”€ i18nSetup.ts
β”‚   β”œβ”€β”€ serieFormCustomization.tsx
β”‚   β”œβ”€β”€ setupConfiguration.ts
β”‚   └── seasonStateMachineExample.ts
β”œβ”€β”€ i18n/                 # Function-based i18n
β”‚   β”œβ”€β”€ en.ts
β”‚   └── es.ts
β”œβ”€β”€ lib/                  # Utilities and configurations
β”‚   β”œβ”€β”€ urqlClient.ts
β”‚   └── themeContext.tsx
└── public/
    └── i18n/            # Static JSON i18n
        β”œβ”€β”€ en.json
        └── es.json

πŸ”„ Data Flow

  1. Schema Introspection β†’ Field Discovery β†’ Form Generation
  2. User Input β†’ Field Change β†’ Customization Processing β†’ State Update
  3. Collection Changes β†’ Local State β†’ Parent Form Integration
  4. Form Submission β†’ Data Validation β†’ GraphQL Mutation β†’ Success/Error Handling

πŸ›  Tech Stack

  • Framework: Next.js 15.4.6 with App Router
  • Language: TypeScript 5 (strict mode)
  • UI Library: Material-UI v7.3.1
  • Data Layer: urql 5.0.1 + GraphQL 16.11.0
  • Styling: Tailwind CSS 4 + Emotion
  • State Management: React hooks + custom hooks
  • Internationalization: i18n support (en/es)
  • Simfinity Components: @simtlix/simfinity-fe-components v1.0.3

Documentation

Core Documentation

  • EntityForm Guide - Complete guide to the EntityForm component architecture, field types, and usage patterns
  • Form Customization - Comprehensive documentation for customizing form layouts, field behavior, validation, and entity-level callbacks

Advanced Features

  • Entity-Level Callbacks - In-depth guide to beforeSubmit, onSuccess, and onError callbacks for custom business logic

Examples & Patterns

  • src/examples/ - Working examples of form customizations, column renderers, i18n setup, and state machines

Learn More

To learn more about the technologies used in this project:

Deploy on Vercel

The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.

Check out the Next.js deployment documentation for more details.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages