Skip to content

Config-Driven UI

Türkçe

ICOSYS'ta UI davranışı (hangi alanlar gösterilir, nasıl sıralanır, filtrelenir, formatlanır) tamamen deklaratif config nesneleri ile tanımlanır. Bileşen seviyesinde JavaScript yazmak gerekmez.

Concept

Every CRUD page in the React frontend is driven by a HandlerConfig object. This config defines:

  • What fields appear in the list table
  • What fields appear in the form (create/edit/view)
  • What filters are available
  • How fields are formatted and decorated
  • What sub-handlers (drill-down tabs) exist
  • What sidebar information is shown
  • What custom actions are available

No JSX is written per entity. The same generic components (CrudListView, CrudFormView, DynamicField) render every entity differently based on its config.


Config Registry

All handler configs are registered in a central registry:

// src/core/config/config-registry.ts

const configRegistry: Record<string, HandlerConfig> = {
    // BPM module
    "business-partners": businessPartnerConfig,
    "branches":          branchConfig,
    "contacts":          contactConfig,
    "contracts":         contractConfig,
    "contract-items":    contractItemConfig,
    "products":          productConfig,

    // DMS module
    "dms-files":      dmsFilesConfig,
    "dms-categories": dmsCategoriesConfig,
    "dms-settings":   dmsSettingsConfig,
    "dms-audit":      dmsAuditConfig,

    // Administration
    "account":     accountConfig,
    "groups":      groupsConfig,
    "group-users": groupUsersConfig,
    // ...
}

// Lookup by basePath
export function getHandlerConfig(basePath: string): HandlerConfig | undefined

// Register at runtime (for dynamic configs)
export function registerHandlerConfig(basePath: string, config: HandlerConfig)

Türkçe

basePath hem URL'de hem config registry'de anahtar olarak kullanılır. Örneğin /business-partners URL'si "business-partners" key'ine karşılık gelir.


HandlerConfig Structure

interface HandlerConfig {
    crud:         CrudConfig           // Core CRUD settings
    filter?:      FilterConfig         // Filter panel
    list:         ListConfig           // Table columns
    form:         FormConfig           // Form sections & fields
    subHandlers?: SubHandlerConfig     // Drill-down tabs
    sidebar?:     SidebarConfig        // Side panel
    actions?:     ActionsConfig        // Custom action buttons
}

crud — Core Settings

crud: {
    basePath: "business-partners",
    moduleName: "ICOSYS_BPM.BusinessPartnerDef",  // RBAC module
    titles: {
        list: "page.bp.list",
        create: "page.bp.create",
        edit: "page.bp.edit",
        view: "page.bp.view",
    },
    idField: "id",                    // Primary key field name
    pageSize: 50,                     // Default rows per page
    pageSizeOptions: [10, 25, 50, 100],
    defaultSort: "legalName",         // Default sort field
    defaultSortOrder: "asc",
    lazy: true,                       // Lazy load list
    exportEnabled: true,              // Show export button
    statsEnabled: true,               // Load entity stats
    displayField: ["code", "legalName"],  // Breadcrumb display
    skipCrProcessFilter: false,       // Include crProcessId in filter
    allowOwner: false,                // Owner bypass for RBAC
    allowAdmin: false,                // Admin bypass for RBAC
}

filter — Filter Panel

filter: {
    enabled: true,
    columns: 3,                       // Grid columns (responsive)
    collapsible: true,                // Can collapse the panel
    defaultCollapsed: false,          // Start collapsed?
    fields: [
        {
            name: "code",
            type: "TEXT",
            label: "field.bp.code",
            colClass: "col-4",
        },
        {
            name: "status",
            type: "SELECT",
            label: "field.bp.status",
            defaultValue: "ACTIVE",   // Pre-selected on load & reset
            options: [
                { label: "option.all", value: "" },
                { label: "status.ACTIVE", value: "ACTIVE" },
                { label: "status.PASSIVE", value: "PASSIVE" },
            ],
        },
        {
            name: "creationDateFrom",
            type: "DATE",
            label: "field.bp.createdFrom",
        },
    ],
    defaultFilterValues: {
        status: "ACTIVE",             // Applied on first load
    },
}

Türkçe

Filtre paneli aktif filtre sayısını badge olarak gösterir. "Reset" butonu defaultFilterValues'a geri döner, tamamen temizlemez.

list — Table Columns

list: {
    fields: [
        {
            name: "code",
            label: "field.bp.code",
            sortable: true,
            width: 120,
        },
        {
            name: "legalName",
            label: "field.bp.legalName",
            sortable: true,
        },
        {
            name: "isCustomer",
            label: "field.bp.isCustomer",
            decorator: "BOOLEAN_ICON",   // ✓ or ✗
            width: 90,
            align: "center",
        },
        {
            name: "status",
            label: "field.bp.status",
            decorator: "BADGE",          // Colored badge
            sortable: true,
            width: 120,
        },
        {
            name: "totalAmount",
            label: "field.bp.totalAmount",
            decorator: "CURRENCY",
            decoratorExpression: "currency",  // Currency field name in row
            align: "right",
        },
    ],
    summary: {                           // Optional footer summary
        type: "CURRENCY_TOTALS",
        amountField: "totalAmount",
        currencyField: "currency",
        label: "Total",
    },
}

form — Sections & Fields

form: {
    sections: [
        {
            key: "basic",
            title: "section.bp.basic",
            icon: "info",
            fields: ["code", "legalName", "shortName"],
        },
        {
            key: "location",
            title: "section.bp.location",
            icon: "map",
            fields: ["countryId", "cityId"],
        },
    ],

    fields: {
        code: {
            name: "code",
            type: "TEXT",
            label: "field.bp.code",
            required: true,
            maxLength: 50,
            colClass: "col-6",
            readOnly: true,              // Not editable in EDIT mode
            visibility: {
                list: true,
                view: true,
                edit: true,
                create: true,
            },
        },
        countryId: {
            name: "countryId",
            type: "SELECT",
            label: "field.bp.countryId",
            colClass: "col-6",
            optionsSource: {
                source: "countries",
                method: "",
            },
        },
        cityId: {
            name: "cityId",
            type: "SELECT",
            label: "field.bp.cityId",
            colClass: "col-6",
            dependsOn: "countryId",      // Cascading dropdown
            optionsSource: {
                source: "cities",
                method: "countryId",     // Refreshes when country changes
            },
        },
    },
}

Visibility control — determines which modes show each field:

visibility: {
    list: true,      // Show in table columns
    view: true,      // Show in VIEW form
    edit: true,      // Show in EDIT form
    create: false,   // Hide in CREATE form
}

Türkçe

readOnly: true olan alanlar EDIT modunda görünür ama değiştirilemez. visibility.edit: false olan alanlar EDIT modunda hiç görünmez. İkisi farklı şeylerdir.


Rendering Pipeline

HandlerConfig
CrudRenderer ─── mode? ──┬── LIST ──► CrudListView
                          │             ├── FilterPanel (filter config)
                          │             ├── DataTable (list.fields)
                          │             └── Pagination
                          └── VIEW/EDIT/CREATE ──► CrudFormView
                                                    ├── FormSections (form.sections)
                                                    │   └── DynamicField[] (form.fields)
                                                    ├── SubHandlerPanel (subHandlers)
                                                    └── Sidebar (sidebar)

DynamicField Rendering

Each field is rendered by DynamicField based on its type and current mode:

// VIEW mode → ReadonlyField with decorator formatting
// EDIT/CREATE mode → EditableField with input component

const DynamicField = memo(({ field, value, mode, onChange, error }) => {
    if (mode === "VIEW") {
        return <ReadonlyField field={field} value={displayValue} />
    }
    if (mode === "EDIT" || mode === "CREATE") {
        return <EditableField field={field} value={value} onChange={onChange} error={error} />
    }
})

EditableField routes to specialized components:

Field Type Component
TEXT, EMAIL <input type="text">
TEXTAREA <textarea>
NUMBER, CURRENCY CurrencyInput / <input type="number">
DATE, LOCAL_DATE <input type="date">
SELECT SearchableSelect
CHECKBOX, SWITCH <input type="checkbox">
PASSWORD <input type="password"> with generate button
LOOKUP_HYBRID LookupHybridField (autocomplete + dialog)
FILE_UPLOAD FileUploadField
TREE_SELECT TreeSelectField
CHIPS ChipsField

DecoratedCell — Display Formatting

In VIEW mode and list tables, values are formatted by decorators:

Decorator Input Output
BOOLEAN_ICON true ✓ (green)
BOOLEAN_ICON false ✗ (red)
BADGE "ACTIVE" Green badge pill
BADGE "PASSIVE" Red badge pill
CURRENCY 1234.5 1,234.50 TRY
DATE "2025-02-15" 02/15/2025
FILE_SIZE 2621440 2.5 MB
EMAIL_LINK "a@b.com" Clickable mailto: link

Custom decorators can be registered at runtime:

registerCustomDecorator("MY_FORMAT", (value, row) => {
    return <span className="text-blue-500">{value}</span>
})

Optional side panel shown in VIEW/EDIT modes:

sidebar: {
    enabled: true,
    width: 300,
    summary: {
        enabled: true,
        title: "sidebar.summary",
        keyFields: [
            { name: "status", decorator: "BADGE" },
            { name: "taxNumber" },
            { name: "isCustomer", decorator: "BOOLEAN_ICON" },
        ],
    },
    relatedCounts: {
        enabled: true,
        title: "sidebar.relatedCounts",
        items: [
            {
                key: "activeBranches",
                label: "sidebar.bp.activeBranches",
                handler: "branches",
                parentFields: [{ source: "id", target: "businessPartnerId" }],
                filter: { status: "true" },
            },
        ],
    },
}

Custom Actions

Define action buttons per mode:

actions: {
    view: [
        {
            key: "activate",
            label: "action.bp.activate",
            icon: "CheckCircle",
            variant: "success",
            confirm: true,
            confirmMessage: "action.bp.confirm.activate",
            renderCondition: "status != 'ACTIVE'",
        },
    ],
    row: [
        {
            key: "quickEdit",
            label: "action.quickEdit",
            icon: "Pencil",
        },
    ],
}
Property Description
variant "default", "success", "danger", "warning"
confirm Show confirmation dialog before executing
renderCondition Expression evaluated against current entity

File Upload Configuration

For entities with file upload (e.g., DMS):

crud: {
    uploadConfig: {
        enabled: true,
        uploadUrl: "/upload",
        fileField: "file",
        metadataFields: ["crProcessId", "entityType", "entityId", "categoryId"],
        fieldMapping: { entityId: "refEntityId" },
    },
}
// Field config for file input
file: {
    name: "file",
    type: "FILE_UPLOAD",
    required: true,
    visibility: { create: true, edit: false, view: false },
    fileUpload: {
        accept: ".pdf,.doc,.docx,.xls,.xlsx,.jpg,.png,.zip",
        maxFileSize: 25 * 1024 * 1024,  // 25 MB
        multiple: false,
    },
}

Adding a New Config

To add a new entity to the frontend:

1. Create config file:

src/core/config/handler-configs/{module}/{entity}.config.ts

2. Define HandlerConfig with crud, list, form, and optional filter, subHandlers, sidebar.

3. Register in config-registry.ts:

import { myEntityConfig } from './handler-configs/mymod/my-entity.config'

const configRegistry = {
    // ...existing configs
    "my-entities": myEntityConfig,
}

4. Add route in routes.tsx:

<Route path="my-entities/*" element={<CrudPage />} />

That's it. The CRUD engine takes over from here.