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>
})
Sidebar Configuration¶
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:
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:
That's it. The CRUD engine takes over from here.