diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7547145..0767a7c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [8.2, 8.3, 8.4]
+ php: [8.3, 8.4]
laravel: [11.*, 12.*]
database: [mysql, pgsql]
include:
diff --git a/README.md b/README.md
index 8859825..0c22e56 100644
--- a/README.md
+++ b/README.md
@@ -247,24 +247,18 @@ use AuroraWebSoftware\FlexyField\Enums\FlexyFieldType;
// Single select (dropdown)
Product::addFieldToSchema(
- 'electronics',
- 'color',
- FlexyFieldType::STRING,
- 100,
- null,
- null,
- ['options' => ['red' => 'Red', 'blue' => 'Blue', 'green' => 'Green']]
+ schemaCode: 'electronics',
+ fieldName: 'color',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['options' => ['red' => 'Red', 'blue' => 'Blue', 'green' => 'Green']]
);
// Multi-select (checkboxes)
Product::addFieldToSchema(
- 'electronics',
- 'features',
- FlexyFieldType::JSON,
- 100,
- null,
- null,
- [
+ schemaCode: 'electronics',
+ fieldName: 'features',
+ fieldType: FlexyFieldType::JSON,
+ fieldMetadata: [
'options' => ['wifi', '5g', 'nfc', 'bluetooth'],
'multiple' => true
]
@@ -278,7 +272,78 @@ $phone->flexy->features = ['wifi', '5g']; // Multiple values
$phone->save();
```
-### Validation
+### Attribute Grouping
+
+Organize related fields into groups for better UI presentation:
+
+```php
+use AuroraWebSoftware\FlexyField\Models\FieldSchema;
+
+// Define fields with groups
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Power Specs']
+);
+
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'weight_kg',
+ fieldType: FlexyFieldType::DECIMAL,
+ fieldMetadata: ['group' => 'Physical Dimensions']
+);
+
+// Fields without group metadata are ungrouped
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING
+);
+
+// Retrieve fields organized by group
+$schema = FieldSchema::where('schema_code', 'electronics')->first();
+$grouped = $schema->getFieldsGrouped();
+
+// Iterate through groups
+foreach ($grouped as $groupName => $fields) {
+ echo "Group: $groupName\n";
+ foreach ($fields as $field) {
+ echo " - {$field->name}\n";
+ }
+}
+```
+
+### UI Hints
+
+Improve UX with human-readable labels, placeholders, and hints:
+
+```php
+use AuroraWebSoftware\FlexyField\Models\SchemaField;
+
+// Define field with UI hints
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'battery_capacity_mah',
+ fieldType: FlexyFieldType::INTEGER,
+ label: 'Battery Capacity',
+ fieldMetadata: [
+ 'placeholder' => 'Enter value in mAh',
+ 'hint' => 'Typical range: 1000-5000mAh'
+ ]
+);
+
+// Retrieve UI hints
+$field = SchemaField::where('name', 'battery_capacity_mah')->first();
+echo $field->getLabel(); // "Battery Capacity"
+echo $field->getPlaceholder(); // "Enter value in mAh"
+echo $field->getHint(); // "Typical range: 1000-5000mAh"
+
+// Label falls back to name if null
+$field->label = null;
+echo $field->getLabel(); // "battery_capacity_mah"
+```
+
```php
try {
diff --git a/composer.json b/composer.json
index a2e4b3a..862d1bd 100644
--- a/composer.json
+++ b/composer.json
@@ -67,16 +67,16 @@
"@php -r \"echo '\\n=== Testing with PostgreSQL ===\\n';\"",
"@composer run test:pgsql"
],
- "test:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist --exclude-filter=FieldSetPerformanceTest",
- "test:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist --exclude-filter=FieldSetPerformanceTest",
+ "test:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist --exclude-group=performance",
+ "test:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist --exclude-group=performance",
"test:performance": [
"@php -r \"echo '\\n=== Performance Tests with MySQL ===\\n';\"",
"@composer run test:performance:mysql",
"@php -r \"echo '\\n=== Performance Tests with PostgreSQL ===\\n';\"",
"@composer run test:performance:pgsql"
],
- "test:performance:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist tests/Feature/FieldSetPerformanceTest.php",
- "test:performance:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist tests/Feature/FieldSetPerformanceTest.php",
+ "test:performance:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist --group=performance",
+ "test:performance:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist --group=performance",
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/pint"
},
@@ -99,4 +99,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
-}
+}
\ No newline at end of file
diff --git a/database/migrations/add_label_column_to_schema_fields.php b/database/migrations/add_label_column_to_schema_fields.php
new file mode 100644
index 0000000..7749011
--- /dev/null
+++ b/database/migrations/add_label_column_to_schema_fields.php
@@ -0,0 +1,26 @@
+string('label')->nullable()->after('name');
+ }
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('ff_schema_fields', function (Blueprint $table) {
+ if (Schema::hasColumn('ff_schema_fields', 'label')) {
+ $table->dropColumn('label');
+ }
+ });
+ }
+};
diff --git a/docs/BEST_PRACTICES.md b/docs/BEST_PRACTICES.md
index ac17711..04a9dd7 100644
--- a/docs/BEST_PRACTICES.md
+++ b/docs/BEST_PRACTICES.md
@@ -82,15 +82,23 @@ public function rules() {
```php
// ✅ Correct
-Product::addFieldToSchema('product', 'tags', FlexyFieldType::JSON, 100, null, null, [
- 'options' => ['new', 'sale', 'featured'],
- 'multiple' => true
-]);
+Product::addFieldToSchema(
+ schemaCode: 'product',
+ fieldName: 'tags',
+ fieldType: FlexyFieldType::JSON,
+ fieldMetadata: [
+ 'options' => ['new', 'sale', 'featured'],
+ 'multiple' => true
+ ]
+);
// ❌ Wrong - STRING type with multiple: true will fail
-Product::addFieldToSchema('product', 'tags', FlexyFieldType::STRING, 100, null, null, [
- 'multiple' => true // Error: multi-select needs JSON type
-]);
+Product::addFieldToSchema(
+ schemaCode: 'product',
+ fieldName: 'tags',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['multiple' => true] // Error: multi-select needs JSON type
+);
```
**Keep options manageable:**
@@ -99,7 +107,66 @@ Product::addFieldToSchema('product', 'tags', FlexyFieldType::STRING, 100, null,
- **Medium lists** (10-50 items): Store in config/constants
- **Large/dynamic lists** (> 50 items): Use relationships instead of select options
-## Data Migration
+## Attribute Grouping
+
+**Organize fields by functional area:**
+
+```php
+// Group related fields together
+['group' => 'Technical Specs'] // voltage, power_consumption, etc.
+['group' => 'Physical'] // weight, dimensions, etc.
+['group' => 'Marketing'] // description, features, etc.
+```
+
+**Naming conventions:**
+
+- **Title Case:** "Power Specs", "Physical Dimensions"
+- **Keep short:** Max ~30 characters for UI readability
+- **Be consistent:** Use same group names across similar schemas
+
+**UI Recommendations:**
+
+- Display groups alphabetically (case-insensitive)
+- Show "Ungrouped" fields last
+- Use collapsible sections for each group
+- Consider icons/colors for visual distinction
+
+## UI Hints
+
+**Use descriptive labels:**
+
+```php
+// ✅ Good
+label: 'Battery Capacity'
+label: 'Product Weight'
+
+// ❌ Bad
+label: 'bat_cap' // Too technical
+label: null // Missing user-friendly name
+```
+
+**Write helpful placeholders:**
+
+```php
+// ✅ Specific format examples
+'placeholder' => 'e.g., 99.99'
+'placeholder' => 'Enter email address'
+
+// ❌ Vague
+'placeholder' => 'Enter value'
+```
+
+**Provide actionable hints:**
+
+```php
+// ✅ Clear constraints
+'hint' => 'Must be between 1000-5000mAh'
+'hint' => 'Leave empty for auto-generation'
+
+// ❌ Redundant
+'hint' => 'Enter battery capacity' // Already in label
+```
+
**Add fields** (no migration needed):
diff --git a/openspec/changes/add-attribute-grouping-support/proposal.md b/openspec/changes/add-attribute-grouping-support/proposal.md
deleted file mode 100644
index 8d70787..0000000
--- a/openspec/changes/add-attribute-grouping-support/proposal.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Change: Add Attribute Grouping Support
-
-## Why
-When a schema has many fields (e.g., 50+ product attributes), displaying them in a single flat list is overwhelming for users. Grouping fields (e.g., "Technical Specs", "Dimensions", "General Info") improves the UI/UX significantly.
-
-## What Changes
-- **Metadata Update:** Utilize the existing `metadata` JSON column in `ff_schema_fields` table.
-- **New Key:** Standardize a `group` key in the metadata array (string value).
-- **Helper:** Add `getGroup()` method to `SchemaField` model.
-- **Collection Macro:** Potentially add a macro or helper to `FieldSchema` to retrieve fields grouped by this key (e.g., `$schema->getFieldsGrouped()`).
-
-## Impact
-- **Affected specs:** `metadata-structure`
-- **Affected code:**
- - `src/Models/SchemaField.php`: Add `getGroup()` helper.
-- **Breaking changes:** None.
-- **Database changes:** None (uses existing `metadata` column).
diff --git a/openspec/changes/add-simple-localization-support/design.md b/openspec/changes/add-simple-localization-support/design.md
new file mode 100644
index 0000000..9c4277a
--- /dev/null
+++ b/openspec/changes/add-simple-localization-support/design.md
@@ -0,0 +1,53 @@
+# Design: Simple Localization Support
+
+## Goal
+Enable storing and retrieving localized content (e.g., product descriptions in multiple languages) within a single field, without creating separate fields for each language.
+
+## Technical Approach
+
+### 1. Storage Strategy
+We will use the existing `FlexyFieldType::JSON` type to store localized data.
+- **Database:** Stored in `value_json` column in `ff_field_values`.
+- **Format:** `{"en": "Blue", "tr": "Mavi", "de": "Blau"}`
+
+### 2. Metadata Configuration
+A new flag `is_translatable` in the `metadata` JSON column of `ff_schema_fields` will indicate if a field is localized.
+
+```json
+{
+ "is_translatable": true
+}
+```
+
+### 3. Accessor Logic (The "Magic")
+We need to intercept access to the field value in `src/Traits/Flexy.php` (specifically `__get`).
+
+- **Current Behavior:** Returns the raw value (string or array).
+- **New Behavior:**
+ - If field is `is_translatable`:
+ - `__get('desc')`: Returns value for current app locale (`app()->getLocale()`).
+ - If current locale missing, fallback to fallback locale.
+ - If both missing, return `null` or empty string.
+
+### 4. Explicit Retrieval
+We need a way to get the raw translations or a specific locale.
+- `$model->flexy->getTranslation('desc', 'tr')`
+- `$model->flexy->getTranslations('desc')` // Returns full array
+
+### 5. Setting Values
+- **Direct Assignment:** `$model->flexy->desc = 'Blue'` -> Sets value for *current locale*, merging with existing JSON.
+- **Bulk Assignment:** `$model->flexy->desc = ['en' => 'Blue', 'tr' => 'Mavi']` -> Replaces/Merges based on array input.
+
+### 6. Validation
+Validation needs to be locale-aware.
+- `required`: At least one locale (or default locale) must have a value.
+- `string`, `min`, `max`: Applied to the value of the *current locale* or all locales?
+ - *Decision:* Apply rules to the *values* within the JSON.
+
+## Constraints
+- Translatable fields **MUST** be `FlexyFieldType::JSON`.
+- Keys in JSON must be valid locale strings (2-5 chars).
+
+## Architecture
+- **SchemaField Model:** Add `isTranslatable()` helper.
+- **Flexy Trait:** Update `__get` and `__set` logic to handle translation merging and retrieval.
diff --git a/openspec/changes/add-simple-localization-support/proposal.md b/openspec/changes/add-simple-localization-support/proposal.md
index 2c7dd96..a5105e5 100644
--- a/openspec/changes/add-simple-localization-support/proposal.md
+++ b/openspec/changes/add-simple-localization-support/proposal.md
@@ -11,6 +11,36 @@ PIM and CMS systems require storing content in multiple languages (e.g., Product
- `$model->flexy->getTranslation('desc', 'en')` for specific locale.
- **Validation:** Update validation logic to validate values inside the JSON structure (e.g., `required` means at least default locale must be present).
+## Usage Example
+
+```php
+use AuroraWebSoftware\FlexyField\Enums\FlexyFieldType;
+
+// Define translatable field
+Product::addFieldToSchema(
+ schemaCode: 'product',
+ fieldName: 'description',
+ fieldType: FlexyFieldType::JSON, // Must be JSON
+ fieldMetadata: ['is_translatable' => true]
+);
+
+// Usage
+app()->setLocale('en');
+$product->flexy->description = 'Blue Shirt'; // Sets {"en": "Blue Shirt"}
+
+app()->setLocale('tr');
+$product->flexy->description = 'Mavi Gömlek'; // Merges: {"en": "Blue Shirt", "tr": "Mavi Gömlek"}
+
+// Retrieval
+echo $product->flexy->description; // Output: "Mavi Gömlek" (current locale)
+echo $product->flexy->getTranslation('description', 'en'); // Output: "Blue Shirt"
+```
+
+## Constraints
+- **Field Type:** Must be `FlexyFieldType::JSON`.
+- **Storage:** Stored as JSON object `{"locale": "value"}`.
+- **Validation:** `required` rule checks if the JSON is not empty.
+
## Impact
- **Affected specs:** `type-system`, `field-validation`
- **Affected code:**
@@ -18,3 +48,13 @@ PIM and CMS systems require storing content in multiple languages (e.g., Product
- `src/Traits/Flexy.php`: Update accessor logic to handle translatable JSON fields.
- **Breaking changes:** None.
- **Database changes:** None (uses existing `JSON` type and `metadata`).
+
+## Requirements
+- **Clean Architecture:** Logic should be encapsulated in Traits or a dedicated Value Object if complex.
+- **Testing:**
+ - Comprehensive feature tests in `tests/Feature/SimpleLocalizationTest.php`.
+ - Test locale switching and fallback.
+- **Quality:**
+ - Must pass `phpstan` (Larastan) level 5+.
+ - Must pass `pint` style checks.
+
diff --git a/openspec/changes/add-simple-localization-support/tasks.md b/openspec/changes/add-simple-localization-support/tasks.md
new file mode 100644
index 0000000..9779a3e
--- /dev/null
+++ b/openspec/changes/add-simple-localization-support/tasks.md
@@ -0,0 +1,41 @@
+# Tasks: Simple Localization Support
+
+## Implementation
+- [ ] Update `src/Models/SchemaField.php`
+ - [ ] Add `isTranslatable(): bool` helper
+- [ ] Update `src/Traits/Flexy.php`
+ - [ ] Modify `__get` to handle localized retrieval
+ - [ ] Modify `__set` to handle localized storage (merging)
+ - [ ] Add `getTranslation(field, locale)` method
+ - [ ] Add `getTranslations(field)` method
+ - [ ] Add `setTranslation(field, locale, value)` method
+
+## Testing
+- [ ] Create `tests/Feature/SimpleLocalizationTest.php`
+ - [ ] Test defining translatable field
+ - [ ] Test setting/getting values in current locale
+ - [ ] Test setting/getting specific locales
+ - [ ] Test fallback locale behavior
+ - [ ] Test bulk assignment (array)
+ - [ ] Test validation (required, string constraints)
+
+## Quality Assurance
+- [ ] Run `phpstan analyse` (Larastan)
+- [ ] Run `pint`
+- [ ] Run full test suite
+
+## Documentation Updates
+- [ ] Update `README.md`
+ - [ ] Add Simple Localization section
+ - [ ] Show translatable field usage
+ - [ ] Document locale switching and fallback
+- [ ] Update `docs/BEST_PRACTICES.md`
+ - [ ] Add localization best practices
+ - [ ] Document when to use translatable fields
+ - [ ] Add tips for managing translations
+- [ ] Update `resources/boost/guidelines/core.blade.php`
+ - [ ] Add localization code snippets
+ - [ ] Document getTranslation() and setTranslation() methods
+- [ ] Update OpenSpec documentation
+ - [ ] Update `openspec/project.md` if needed
+ - [ ] Update type system documentation
diff --git a/openspec/changes/add-ui-hints-support/proposal.md b/openspec/changes/add-ui-hints-support/proposal.md
deleted file mode 100644
index bc6933e..0000000
--- a/openspec/changes/add-ui-hints-support/proposal.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Change: Add UI Hints Support
-
-## Why
-Field names (e.g., `battery_capacity_mah`) are often technical and not user-friendly. Users need guidance, placeholders, and readable labels in the UI to enter data correctly.
-
-## What Changes
-- **Metadata Update:** Utilize the existing `metadata` JSON column in `ff_schema_fields` table.
-- **New Keys:** Standardize the following keys:
- - `label`: Human-readable label (e.g., "Battery Capacity").
- - `placeholder`: Input placeholder text (e.g., "Enter value in mAh").
- - `hint`: Help text or tooltip (e.g., "Must be between 1000 and 5000").
-- **Helpers:** Add methods to `SchemaField` to retrieve these UI attributes easily.
-
-## Impact
-- **Affected specs:** `metadata-structure`
-- **Affected code:**
- - `src/Models/SchemaField.php`: Add helpers (`getLabel()`, `getPlaceholder()`, `getHint()`).
-- **Breaking changes:** None.
-- **Database changes:** None (uses existing `metadata` column).
diff --git a/openspec/changes/archive/add-attribute-grouping-support/design.md b/openspec/changes/archive/add-attribute-grouping-support/design.md
new file mode 100644
index 0000000..010a74f
--- /dev/null
+++ b/openspec/changes/archive/add-attribute-grouping-support/design.md
@@ -0,0 +1,49 @@
+# Design: Attribute Grouping Support
+
+## Goal
+Allow fields within a schema to be grouped logically (e.g., "Technical Specs", "Dimensions", "General") to improve UI organization in PIM/CRM applications.
+
+## Technical Approach
+
+### 1. Metadata Storage
+We will use the existing `metadata` JSON column in the `ff_schema_fields` table. No database schema changes are required.
+
+**Structure:**
+```json
+{
+ "group": "Technical Specs",
+ "options": [...] // existing feature
+}
+```
+
+### 2. SchemaField Model Updates
+Add helper methods to `src/Models/SchemaField.php` to easily access group information.
+
+- `getGroup(): ?string`: Returns the group name or null if not grouped.
+- `hasGroup(): bool`: Returns true if the field belongs to a group.
+
+### 3. FieldSchema Model Updates
+Add a helper method to `src/Models/FieldSchema.php` to retrieve fields organized by group.
+
+- `getFieldsGrouped(): Collection`: Returns a collection where keys are group names (or "default") and values are collections of `SchemaField` objects.
+
+### 4. API / Usage
+The `addFieldToSchema` method already accepts `fieldMetadata`. We will simply document and standardize the usage of the `group` key.
+
+```php
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Power Specs']
+);
+```
+
+## Constraints
+- Group names are simple strings.
+- If `group` is missing or null, the field belongs to the "default" or "ungrouped" section.
+- Implementation must be backward compatible.
+
+## Architecture
+- **Clean & Simple:** Logic resides in Model accessors/helpers. No complex services or traits needed.
+- **Performance:** Grouping happens in memory (Collection) after fetching fields. Since schema fields are usually limited (<100), this is performant.
diff --git a/openspec/changes/archive/add-attribute-grouping-support/proposal.md b/openspec/changes/archive/add-attribute-grouping-support/proposal.md
new file mode 100644
index 0000000..b3c8b55
--- /dev/null
+++ b/openspec/changes/archive/add-attribute-grouping-support/proposal.md
@@ -0,0 +1,79 @@
+# Change: Add Attribute Grouping Support
+
+## Why
+When a schema has many fields (e.g., 50+ product attributes), displaying them in a single flat list is overwhelming for users. Grouping fields (e.g., "Technical Specs", "Dimensions", "General Info") improves the UI/UX significantly.
+
+## What Changes
+- **Metadata Update:** Utilize the existing `metadata` JSON column in `ff_schema_fields` table.
+- **New Key:** Standardize a `group` key in the metadata array (string value).
+- **Helper:** Add `getGroup()` method to `SchemaField` model.
+- **Collection Helper:** Add `getFieldsGrouped()` to `FieldSchema` to retrieve fields grouped by this key.
+
+## Usage Example
+
+```php
+use AuroraWebSoftware\FlexyField\Enums\FlexyFieldType;
+
+// Define fields with groups
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Power Specs']
+);
+
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'weight_kg',
+ fieldType: FlexyFieldType::DECIMAL,
+ fieldMetadata: ['group' => 'Physical Dimensions']
+);
+
+// Fields without group metadata will be ungrouped
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING
+);
+
+// Retrieve grouped fields
+$schema = FieldSchema::where('schema_code', 'electronics')->first();
+$grouped = $schema->getFieldsGrouped();
+// Returns: ['Power Specs' => [...], 'Physical Dimensions' => [...], 'Ungrouped' => [...]]
+```
+
+## Constraints
+- **Group Name:** Any string value. Empty strings (`""`) treated as ungrouped.
+- **Special Characters:** Fully supported (emoji, international characters, etc.).
+- **Length:** No hard limit (practical UI limit ~50 chars).
+- **Case Sensitivity:** Preserved as-is. `"Power Specs"` ≠ `"power specs"`.
+- **Order:**
+ - Groups displayed in alphabetical order (case-insensitive).
+ - "Ungrouped" fields always appear **last**.
+ - Fields within each group sorted by existing `sort` column.
+
+## Impact
+- **Affected specs:** `metadata-structure`
+- **Affected code:**
+ - `src/Models/SchemaField.php`: Add `getGroup()` and `hasGroup()` helpers.
+ - `src/Models/FieldSchema.php`: Add `getFieldsGrouped()` helper.
+- **Breaking changes:** None.
+- **Database changes:** None (uses existing `metadata` column).
+
+## Requirements
+- **Clean Architecture:** Keep logic simple and contained within Models. Avoid unnecessary service layers.
+- **Testing:**
+ - Comprehensive feature tests in `tests/Feature/AttributeGroupingTest.php`.
+ - Edge case coverage (null groups, empty strings, special characters, sorting).
+- **Quality:**
+ - Must pass `phpstan` (Larastan) level 5+.
+ - Must pass `pint` style checks.
+- **Documentation:**
+ - Update `README.md` with usage examples.
+ - Update `docs/BEST_PRACTICES.md` with grouping best practices.
+ - Update `resources/boost/guidelines/core.blade.php` for AI assistants.
+
+## Notes
+- Groups are purely metadata-driven; no database schema changes required.
+- This feature is **optional** - existing schemas without groups continue to work unchanged.
+- UI rendering is left to the consuming application (this package provides data structure only).
diff --git a/openspec/changes/archive/add-attribute-grouping-support/tasks.md b/openspec/changes/archive/add-attribute-grouping-support/tasks.md
new file mode 100644
index 0000000..9a4faeb
--- /dev/null
+++ b/openspec/changes/archive/add-attribute-grouping-support/tasks.md
@@ -0,0 +1,25 @@
+# Tasks: Attribute Grouping Support
+
+## Implementation
+- [ ] Update `src/Models/SchemaField.php`
+ - [ ] Add `getGroup(): ?string` method
+ - [ ] Add `hasGroup(): bool` method
+- [ ] Update `src/Models/FieldSchema.php`
+ - [ ] Add `getFieldsGrouped(): Collection` method
+
+## Testing
+- [ ] Create `tests/Feature/AttributeGroupingTest.php`
+ - [ ] Test assigning groups via `addFieldToSchema`
+ - [ ] Test `getGroup()` and `hasGroup()` helpers
+ - [ ] Test `getFieldsGrouped()` returns correct structure
+ - [ ] Test mixed grouped and ungrouped fields
+ - [ ] Test empty/null groups
+
+## Quality Assurance
+- [ ] Run `phpstan analyse` (Larastan) - Ensure strict type safety
+- [ ] Run `pint` - Ensure code style compliance
+- [ ] Verify no regression in existing tests
+
+## Documentation
+- [ ] Update `README.md` with grouping examples
+- [ ] Update `docs/BEST_PRACTICES.md`
diff --git a/openspec/changes/archive/add-ui-hints-support/design.md b/openspec/changes/archive/add-ui-hints-support/design.md
new file mode 100644
index 0000000..d9deabb
--- /dev/null
+++ b/openspec/changes/archive/add-ui-hints-support/design.md
@@ -0,0 +1,53 @@
+# Design: UI Hints Support
+
+## Goal
+Provide user-friendly labels, placeholders, and hints for fields to improve UI/UX.
+
+## Technical Approach
+
+### 1. Label Column (New Database Column)
+Add a dedicated `label` column to `ff_schema_fields` table.
+
+**Reason:** Label is a fundamental field property, used in exports, APIs, and validation messages.
+
+**Migration:**
+```php
+Schema::table('ff_schema_fields', function (Blueprint $table) {
+ $table->string('label')->nullable()->after('name');
+});
+```
+
+### 2. Metadata for UI Decoration
+Store optional UI hints in `metadata` JSON column:
+- `placeholder`: Input placeholder text
+- `hint`: Help text or tooltip
+
+**Format:**
+```json
+{
+ "placeholder": "Enter value in mAh",
+ "hint": "Max 5000mAh"
+}
+```
+
+### 3. SchemaField Model Updates
+Add helper methods:
+- `getLabel(): string` - Returns `label` or falls back to `name`
+- `getPlaceholder(): ?string` - Returns placeholder from metadata
+- `getHint(): ?string` - Returns hint from metadata
+
+### 4. Default Behavior
+If `label` is null, fallback to `name` field for backward compatibility.
+
+## Constraints
+- `label` is optional (nullable)
+- `placeholder` and `hint` are optional metadata
+- No validation changes required
+
+## Database Schema
+```sql
+ff_schema_fields:
+ - name (slug)
+ - label (display name) ← NEW COLUMN
+ - metadata (JSON: {placeholder, hint, ...})
+```
diff --git a/openspec/changes/archive/add-ui-hints-support/proposal.md b/openspec/changes/archive/add-ui-hints-support/proposal.md
new file mode 100644
index 0000000..c3919b8
--- /dev/null
+++ b/openspec/changes/archive/add-ui-hints-support/proposal.md
@@ -0,0 +1,55 @@
+# Change: Add UI Hints Support
+
+## Why
+Field names (e.g., `battery_capacity_mah`) are often technical and not user-friendly. Users need guidance, placeholders, and readable labels in the UI to enter data correctly.
+
+## What Changes
+- **New Column:** Add `label` column to `ff_schema_fields` for human-readable display names.
+- **Metadata Update:** Use `metadata` JSON for optional UI decorations:
+ - `placeholder`: Input placeholder text
+ - `hint`: Help text or tooltip
+- **Helpers:** Add methods to `SchemaField` to retrieve UI attributes.
+
+## Usage Example
+
+```php
+// Define field with UI hints
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'battery_capacity_mah',
+ fieldType: FlexyFieldType::INTEGER,
+ label: 'Battery Capacity',
+ fieldMetadata: [
+ 'placeholder' => 'Enter mAh',
+ 'hint' => 'Max 5000mAh'
+ ]
+);
+
+// Usage
+$field = SchemaField::where('name', 'battery_capacity_mah')->first();
+echo $field->getLabel(); // "Battery Capacity"
+echo $field->getPlaceholder(); // "Enter mAh"
+echo $field->getHint(); // "Max 5000mAh"
+
+// If label is null, falls back to name
+$field->label = null;
+echo $field->getLabel(); // "battery_capacity_mah"
+```
+
+## Impact
+- **Affected specs:** `metadata-structure`
+- **Affected code:**
+ - Migration: Add `label` column
+ - `src/Models/SchemaField.php`: Add `getLabel()`, `getPlaceholder()`, `getHint()`
+- **Breaking changes:** None (label column is nullable)
+- **Database changes:** New `label` column in `ff_schema_fields`
+
+## Requirements
+- **Clean Architecture:** Simple helper methods, no complex logic
+- **Testing:**
+ - Comprehensive tests in `tests/Feature/UIHintsTest.php`
+ - Test label fallback behavior
+ - Test null/empty values
+- **Quality:**
+ - Must pass `phpstan` (Larastan)
+ - Must pass `pint`
diff --git a/openspec/changes/archive/add-ui-hints-support/specs/field-set-management/spec.md b/openspec/changes/archive/add-ui-hints-support/specs/field-set-management/spec.md
new file mode 100644
index 0000000..e10487b
--- /dev/null
+++ b/openspec/changes/archive/add-ui-hints-support/specs/field-set-management/spec.md
@@ -0,0 +1,38 @@
+## ADDED Requirements
+### Requirement: UI Hints for Fields
+The system SHALL support UI hints for fields to provide better user experience in forms and interfaces.
+
+#### Scenario: Field has label UI hint
+- **WHEN** a field is created with metadata containing 'label' key
+- **THEN** the label SHALL be stored in the metadata JSON column
+- **AND** the label SHALL be retrievable via getLabel() method
+- **AND** if no label is provided, the field name SHALL be used as fallback
+
+#### Scenario: Field has placeholder UI hint
+- **WHEN** a field is created with metadata containing 'placeholder' key
+- **THEN** the placeholder text SHALL be stored in the metadata JSON column
+- **AND** the placeholder SHALL be retrievable via getPlaceholder() method
+- **AND** null SHALL be returned if no placeholder is provided
+
+#### Scenario: Field has hint UI hint
+- **WHEN** a field is created with metadata containing 'hint' key
+- **THEN** the hint text SHALL be stored in the metadata JSON column
+- **AND** the hint SHALL be retrievable via getHint() method
+- **AND** null SHALL be returned if no hint is provided
+
+#### Scenario: Field has multiple UI hints
+- **WHEN** a field is created with metadata containing 'label', 'placeholder', and 'hint' keys
+- **THEN** all UI hints SHALL be stored in the metadata JSON column
+- **AND** each UI hint SHALL be retrievable via its respective method
+- **AND** existing metadata SHALL be preserved alongside UI hints
+
+## MODIFIED Requirements
+### Requirement: Field has metadata
+The system SHALL support rich metadata for fields, including UI hints for better user experience.
+
+#### Scenario: Field is added with metadata array
+- **WHEN** a field is added with metadata array
+- **THEN** the metadata SHALL be stored as JSON in the metadata column
+- **AND** metadata MAY include UI hints (label, placeholder, hint), help text, or custom properties
+- **AND** metadata SHALL be retrievable with the field definition
+- **AND** UI hints SHALL be accessible via dedicated getter methods
diff --git a/openspec/changes/archive/add-ui-hints-support/tasks.md b/openspec/changes/archive/add-ui-hints-support/tasks.md
new file mode 100644
index 0000000..08593ef
--- /dev/null
+++ b/openspec/changes/archive/add-ui-hints-support/tasks.md
@@ -0,0 +1,38 @@
+# Tasks: UI Hints Support
+
+## Database Migration
+- [ ] Create migration to add `label` column
+- [ ] Add `label` to `$fillable` in SchemaField model
+
+## Model Updates
+- [ ] Update `src/Models/SchemaField.php`
+ - [ ] Add `getLabel(): string` method (fallback to name)
+ - [ ] Add `getPlaceholder(): ?string` method
+ - [ ] Add `getHint(): ?string` method
+
+## Testing
+- [ ] Create `tests/Feature/UIHintsTest.php`
+ - [ ] Test label storage and retrieval
+ - [ ] Test label fallback to name
+ - [ ] Test placeholder storage and retrieval
+ - [ ] Test hint storage and retrieval
+ - [ ] Test null/empty values
+
+## Quality Assurance
+- [ ] Run `phpstan analyse`
+- [ ] Run `pint`
+- [ ] Run full test suite
+
+## Documentation Updates
+- [ ] Update `README.md`
+ - [ ] Add UI Hints section with usage examples
+ - [ ] Show label, placeholder, and hint usage
+- [ ] Update `docs/BEST_PRACTICES.md`
+ - [ ] Add best practices for label naming
+ - [ ] Add guidelines for placeholder and hint text
+- [ ] Update `resources/boost/guidelines/core.blade.php`
+ - [ ] Add UI Hints code snippets
+ - [ ] Document label fallback behavior
+- [ ] Update OpenSpec documentation
+ - [ ] Update `openspec/project.md` if needed
+ - [ ] Update metadata structure documentation
diff --git a/resources/boost/guidelines/core.blade.php b/resources/boost/guidelines/core.blade.php
index 35da5bd..3477e55 100644
--- a/resources/boost/guidelines/core.blade.php
+++ b/resources/boost/guidelines/core.blade.php
@@ -164,24 +164,18 @@ class Product extends Model implements FlexyModelContract
// Indexed array (values are both keys and labels)
Product::addFieldToSchema(
- 'electronics',
- 'size',
- FlexyFieldType::STRING,
- 100,
- null,
- null,
- ['options' => ['S', 'M', 'L', 'XL']]
+ schemaCode: 'electronics',
+ fieldName: 'size',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['options' => ['S', 'M', 'L', 'XL']]
);
// Associative array (keys stored, values for display)
Product::addFieldToSchema(
- 'electronics',
- 'color',
- FlexyFieldType::STRING,
- 100,
- null,
- null,
- ['options' => ['red' => 'Red', 'blue' => 'Blue', 'green' => 'Green']]
+ schemaCode: 'electronics',
+ fieldName: 'color',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['options' => ['red' => 'Red', 'blue' => 'Blue', 'green' => 'Green']]
);
// Usage
@@ -196,13 +190,10 @@ class Product extends Model implements FlexyModelContract
// Multi-select requires FlexyFieldType::JSON
Product::addFieldToSchema(
- 'electronics',
- 'features',
- FlexyFieldType::JSON, // MUST be JSON type
- 100,
- null,
- null,
- [
+ schemaCode: 'electronics',
+ fieldName: 'features',
+ fieldType: FlexyFieldType::JSON, // MUST be JSON type
+ fieldMetadata: [
'options' => ['wifi', '5g', 'nfc', 'bluetooth'],
'multiple' => true // Enable multi-select
]
@@ -223,7 +214,109 @@ class Product extends Model implements FlexyModelContract
- For indexed arrays, values are used for both storage and validation
- For associative arrays, keys are used for storage and validation
-### Validation
+### Attribute Grouping
+
+Organize related fields into groups for better UI organization. Especially useful in PIM/CRM applications:
+
+@verbatim
+
+use AuroraWebSoftware\FlexyField\Models\FieldSchema;
+
+// Define fields with group metadata
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Power Specs']
+);
+
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'weight_kg',
+ fieldType: FlexyFieldType::DECIMAL,
+ fieldMetadata: ['group' => 'Physical Dimensions']
+);
+
+// Fields without group metadata are ungrouped
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING
+);
+
+@endverbatim
+
+@verbatim
+
+// Retrieve fields organized by group
+$schema = FieldSchema::where('schema_code', 'electronics')->first();
+$grouped = $schema->getFieldsGrouped();
+
+// Iterate through groups
+foreach ($grouped as $groupName => $fields) {
+ echo "Group: $groupName\n";
+ foreach ($fields as $field) {
+ echo " - {$field->name}\n";
+ }
+}
+
+// Groups are sorted alphabetically (case-insensitive)
+// "Ungrouped" fields always appear last
+// Fields within each group are sorted by their 'sort' column
+
+@endverbatim
+
+**Grouping Rules:**
+- Group name is stored in `metadata['group']` field
+- Empty strings (`""`) are treated as ungrouped
+- Groups are sorted alphabetically (case-insensitive)
+- "Ungrouped" fields always appear last
+- Fields within groups use existing `sort` column for ordering
+
+### UI Hints
+
+Improve UX with labels, placeholders, and hints:
+
+@verbatim
+
+use AuroraWebSoftware\FlexyField\Models\SchemaField;
+
+// Define field with all UI hints
+Product::addFieldToSchema(
+ schemaCode: 'electronics',
+ fieldName: 'battery_capacity_mah',
+ fieldType: FlexyFieldType::INTEGER,
+ label: 'Battery Capacity', // Human-readable label
+ fieldMetadata: [
+ 'placeholder' => 'Enter mAh', // Input placeholder
+ 'hint' => 'Range: 1000-5000mAh' // Help text
+ ]
+);
+
+@endverbatim
+
+@verbatim
+
+$field = SchemaField::where('name', 'battery_capacity_mah')->first();
+
+// Get UI hints
+echo $field->getLabel(); // "Battery Capacity"
+echo $field->getPlaceholder(); // "Enter mAh"
+echo $field->getHint(); // "Range: 1000-5000mAh"
+
+// Label falls back to field name if null/empty
+$field->label = null;
+echo $field->getLabel(); // "battery_capacity_mah"
+
+@endverbatim
+
+**Important Rules:**
+- `label` is stored in dedicated column (`ff_schema_fields.label`)
+- `placeholder` and `hint` are stored in `metadata` JSON
+- `getLabel()` always returns a string (fallback to name)
+- `getPlaceholder()` and `getHint()` return null if not set
+- Empty label strings fallback to name
+
Validation is enforced when saving. Models must be assigned to schema first:
diff --git a/src/Models/FieldSchema.php b/src/Models/FieldSchema.php
index 0fdc1bf..5816f09 100644
--- a/src/Models/FieldSchema.php
+++ b/src/Models/FieldSchema.php
@@ -148,4 +148,33 @@ public function isInUse(string $modelClass): bool
{
return $this->getUsageCount($modelClass) > 0;
}
+
+ /**
+ * Get fields organized by group
+ *
+ * Returns a collection where keys are group names (or "Ungrouped") and values are collections of SchemaField objects.
+ * Groups are sorted alphabetically (case-insensitive), with "Ungrouped" always last.
+ * Fields within each group are sorted by their 'sort' column.
+ */
+ /** @phpstan-ignore-next-line */
+ public function getFieldsGrouped(): \Illuminate\Database\Eloquent\Collection
+ {
+ $fields = $this->fields()->orderBy('sort')->get();
+
+ $grouped = $fields->groupBy(function (SchemaField $field) {
+ return $field->getGroup() ?? 'Ungrouped';
+ });
+
+ // Sort groups alphabetically (case-insensitive), but keep "Ungrouped" last
+ return $grouped->sortKeysUsing(function ($a, $b) {
+ if ($a === 'Ungrouped') {
+ return 1;
+ }
+ if ($b === 'Ungrouped') {
+ return -1;
+ }
+
+ return strcasecmp($a, $b);
+ });
+ }
}
diff --git a/src/Models/SchemaField.php b/src/Models/SchemaField.php
index 1c6efb1..77ab2a1 100644
--- a/src/Models/SchemaField.php
+++ b/src/Models/SchemaField.php
@@ -11,6 +11,7 @@
* @property string $schema_code
* @property int|null $schema_id
* @property string $name
+ * @property string|null $label
* @property FlexyFieldType $type
* @property int $sort
* @property string|array|null $validation_rules
@@ -183,4 +184,55 @@ protected function getOptionsKeys(array $options): array
// For associative arrays, use keys; for indexed arrays, use values
return array_is_list($options) ? $options : array_keys($options);
}
+
+ /**
+ * Get the group name for this field
+ */
+ public function getGroup(): ?string
+ {
+ $group = $this->metadata['group'] ?? null;
+
+ // Treat empty strings as ungrouped
+ if ($group === '' || $group === null) {
+ return null;
+ }
+
+ return (string) $group;
+ }
+
+ /**
+ * Check if this field belongs to a group
+ */
+ public function hasGroup(): bool
+ {
+ return $this->getGroup() !== null;
+ }
+
+ /**
+ * Get the label for this field (falls back to name)
+ */
+ public function getLabel(): string
+ {
+ if (! empty($this->label)) {
+ return $this->label;
+ }
+
+ return $this->name;
+ }
+
+ /**
+ * Get the placeholder text for this field
+ */
+ public function getPlaceholder(): ?string
+ {
+ return $this->metadata['placeholder'] ?? null;
+ }
+
+ /**
+ * Get the hint text for this field
+ */
+ public function getHint(): ?string
+ {
+ return $this->metadata['hint'] ?? null;
+ }
}
diff --git a/src/Traits/Flexy.php b/src/Traits/Flexy.php
index 1db7310..c5fbfdb 100644
--- a/src/Traits/Flexy.php
+++ b/src/Traits/Flexy.php
@@ -260,7 +260,8 @@ public static function addFieldToSchema(
int $sort = 100,
?string $validationRules = null,
?array $validationMessages = null,
- ?array $fieldMetadata = null
+ ?array $fieldMetadata = null,
+ ?string $label = null
): SchemaField {
$modelType = static::getModelType();
@@ -277,6 +278,7 @@ public static function addFieldToSchema(
'schema_code' => $schemaCode,
'schema_id' => $schema->id,
'name' => $fieldName,
+ 'label' => $label,
'type' => $fieldType,
'sort' => $sort,
'validation_rules' => $validationRules,
diff --git a/tests/Feature/AttributeGroupingTest.php b/tests/Feature/AttributeGroupingTest.php
new file mode 100644
index 0000000..961f8bf
--- /dev/null
+++ b/tests/Feature/AttributeGroupingTest.php
@@ -0,0 +1,241 @@
+id();
+ $table->string('name');
+ $table->string('schema_code')->nullable()->index();
+ $table->timestamps();
+ });
+
+ $this->cleanupTestData();
+});
+
+afterEach(function () {
+ Schema::dropIfExists('ff_example_flexy_models');
+ $this->cleanupTestData();
+});
+
+it('stores group in metadata', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Power Specs']
+ );
+
+ expect($field->metadata)->toBeArray()
+ ->and($field->metadata['group'])->toBe('Power Specs');
+});
+
+it('returns group name via getGroup()', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Technical Specs']
+ );
+
+ expect($field->getGroup())->toBe('Technical Specs');
+});
+
+it('returns null for ungrouped fields', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING
+ );
+
+ expect($field->getGroup())->toBeNull()
+ ->and($field->hasGroup())->toBeFalse();
+});
+
+it('treats empty string as ungrouped', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => '']
+ );
+
+ expect($field->getGroup())->toBeNull()
+ ->and($field->hasGroup())->toBeFalse();
+});
+
+it('hasGroup returns true for grouped fields', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Power']
+ );
+
+ expect($field->hasGroup())->toBeTrue();
+});
+
+it('organizes fields by group via getFieldsGrouped()', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ // Add fields to different groups
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ sort: 1,
+ fieldMetadata: ['group' => 'Power Specs']
+ );
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'weight',
+ fieldType: FlexyFieldType::DECIMAL,
+ sort: 2,
+ fieldMetadata: ['group' => 'Physical']
+ );
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING,
+ sort: 3
+ // No group
+ );
+
+ $schema = FieldSchema::where('schema_code', 'test')->first();
+ $grouped = $schema->getFieldsGrouped();
+
+ expect($grouped)->toBeInstanceOf(\Illuminate\Support\Collection::class)
+ ->and($grouped->keys()->toArray())->toContain('Power Specs')
+ ->and($grouped->keys()->toArray())->toContain('Physical')
+ ->and($grouped->keys()->toArray())->toContain('Ungrouped')
+ ->and($grouped['Power Specs'])->toHaveCount(1)
+ ->and($grouped['Physical'])->toHaveCount(1)
+ ->and($grouped['Ungrouped'])->toHaveCount(1);
+});
+
+it('sorts groups alphabetically with Ungrouped last', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field1',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Zebra Group']
+ );
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field2',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Alpha Group']
+ );
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field3',
+ fieldType: FlexyFieldType::STRING
+ // Ungrouped
+ );
+
+ $schema = FieldSchema::where('schema_code', 'test')->first();
+ $grouped = $schema->getFieldsGrouped();
+ $keys = $grouped->keys()->toArray();
+
+ expect($keys[0])->toBe('Alpha Group')
+ ->and($keys[1])->toBe('Zebra Group')
+ ->and($keys[2])->toBe('Ungrouped');
+});
+
+it('sorts fields within groups by sort column', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field_c',
+ fieldType: FlexyFieldType::STRING,
+ sort: 30,
+ fieldMetadata: ['group' => 'Group A']
+ );
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field_a',
+ fieldType: FlexyFieldType::STRING,
+ sort: 10,
+ fieldMetadata: ['group' => 'Group A']
+ );
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field_b',
+ fieldType: FlexyFieldType::STRING,
+ sort: 20,
+ fieldMetadata: ['group' => 'Group A']
+ );
+
+ $schema = FieldSchema::where('schema_code', 'test')->first();
+ $grouped = $schema->getFieldsGrouped();
+ $fields = $grouped['Group A'];
+
+ expect($fields[0]->name)->toBe('field_a')
+ ->and($fields[1]->name)->toBe('field_b')
+ ->and($fields[2]->name)->toBe('field_c');
+});
+
+it('handles special characters in group names', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field1',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Güç Özellikleri 🔋']
+ );
+
+ expect($field->getGroup())->toBe('Güç Özellikleri 🔋');
+});
+
+it('handles case sensitivity in group names', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field1',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'Power Specs']
+ );
+
+ ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field2',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['group' => 'power specs']
+ );
+
+ $schema = FieldSchema::where('schema_code', 'test')->first();
+ $grouped = $schema->getFieldsGrouped();
+
+ // Both groups should exist separately (case preserved)
+ expect($grouped->keys()->toArray())->toContain('Power Specs')
+ ->and($grouped->keys()->toArray())->toContain('power specs');
+});
diff --git a/tests/Feature/SchemaPerformanceTest.php b/tests/Feature/SchemaPerformanceTest.php
index b5a10df..342d339 100644
--- a/tests/Feature/SchemaPerformanceTest.php
+++ b/tests/Feature/SchemaPerformanceTest.php
@@ -7,7 +7,7 @@
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
-uses(CreatesSchemas::class);
+uses(CreatesSchemas::class)->group('performance');
beforeEach(function () {
diff --git a/tests/Feature/UIHintsTest.php b/tests/Feature/UIHintsTest.php
new file mode 100644
index 0000000..cc225fc
--- /dev/null
+++ b/tests/Feature/UIHintsTest.php
@@ -0,0 +1,168 @@
+id();
+ $table->string('name');
+ $table->string('schema_code')->nullable()->index();
+ $table->timestamps();
+ });
+
+ $this->cleanupTestData();
+});
+
+afterEach(function () {
+ Schema::dropIfExists('ff_example_flexy_models');
+ $this->cleanupTestData();
+});
+
+it('stores label in database', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'battery_capacity_mah',
+ fieldType: FlexyFieldType::INTEGER,
+ label: 'Battery Capacity'
+ );
+
+ expect($field->label)->toBe('Battery Capacity')
+ ->and($field->name)->toBe('battery_capacity_mah');
+});
+
+it('getLabel returns label when present', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ label: 'Voltage Rating'
+ );
+
+ expect($field->getLabel())->toBe('Voltage Rating');
+});
+
+it('getLabel falls back to name when label is null', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'battery_capacity_mah',
+ fieldType: FlexyFieldType::INTEGER
+ );
+
+ expect($field->label)->toBeNull()
+ ->and($field->getLabel())->toBe('battery_capacity_mah');
+});
+
+it('stores and retrieves placeholder from metadata', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'voltage',
+ fieldType: FlexyFieldType::STRING,
+ fieldMetadata: ['placeholder' => 'Enter voltage in V']
+ );
+
+ expect($field->getPlaceholder())->toBe('Enter voltage in V');
+});
+
+it('stores and retrieves hint from metadata', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'capacity',
+ fieldType: FlexyFieldType::INTEGER,
+ fieldMetadata: ['hint' => 'Max 5000mAh']
+ );
+
+ expect($field->getHint())->toBe('Max 5000mAh');
+});
+
+it('returns null for missing placeholder', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING
+ );
+
+ expect($field->getPlaceholder())->toBeNull();
+});
+
+it('returns null for missing hint', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'name',
+ fieldType: FlexyFieldType::STRING
+ );
+
+ expect($field->getHint())->toBeNull();
+});
+
+it('handles all UI hints together', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'battery_capacity_mah',
+ fieldType: FlexyFieldType::INTEGER,
+ label: 'Battery Capacity',
+ fieldMetadata: [
+ 'placeholder' => 'Enter mAh',
+ 'hint' => 'Between 1000-5000mAh',
+ ]
+ );
+
+ expect($field->getLabel())->toBe('Battery Capacity')
+ ->and($field->getPlaceholder())->toBe('Enter mAh')
+ ->and($field->getHint())->toBe('Between 1000-5000mAh');
+});
+
+it('handles empty string label as null', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'field_name',
+ fieldType: FlexyFieldType::STRING,
+ label: ''
+ );
+
+ // Empty string is stored as-is but getLabel() falls back to name
+ expect($field->getLabel())->toBe('field_name');
+});
+
+it('preserves special characters in UI hints', function () {
+ ExampleFlexyModel::createSchema('test', 'Test Schema');
+
+ $field = ExampleFlexyModel::addFieldToSchema(
+ schemaCode: 'test',
+ fieldName: 'price',
+ fieldType: FlexyFieldType::DECIMAL,
+ label: 'Fiyat 💰',
+ fieldMetadata: [
+ 'placeholder' => 'Örnek: 99.99 ₺',
+ 'hint' => 'Maksimum 10.000 ₺',
+ ]
+ );
+
+ expect($field->getLabel())->toBe('Fiyat 💰')
+ ->and($field->getPlaceholder())->toBe('Örnek: 99.99 ₺')
+ ->and($field->getHint())->toBe('Maksimum 10.000 ₺');
+});
diff --git a/tests/Feature/ViewRecreationPerformanceTest.php b/tests/Feature/ViewRecreationPerformanceTest.php
index 2492e0e..0996245 100644
--- a/tests/Feature/ViewRecreationPerformanceTest.php
+++ b/tests/Feature/ViewRecreationPerformanceTest.php
@@ -6,7 +6,7 @@
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
use Illuminate\Support\Facades\Schema;
-uses(CreatesSchemas::class);
+uses(CreatesSchemas::class)->group('performance');
beforeEach(function () {
\Illuminate\Support\Facades\Schema::dropIfExists('ff_example_flexy_models');
diff --git a/tests/PackageTest.php b/tests/PackageTest.php
index ca4ec6f..b1cd53a 100644
--- a/tests/PackageTest.php
+++ b/tests/PackageTest.php
@@ -4,15 +4,14 @@
use AuroraWebSoftware\FlexyField\Tests\Concerns\CreatesSchemas;
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
uses(CreatesSchemas::class);
beforeEach(function () {
-
- Artisan::call('migrate:fresh');
+ // Don't call migrate:fresh - it doesn't run package migrations
+ // TestCase already handles migrations via runMigrations()
Schema::dropIfExists('ff_example_flexy_models');
Schema::create('ff_example_flexy_models', function (Blueprint $table) {
@@ -21,6 +20,9 @@
$table->string('schema_code')->nullable();
$table->timestamps();
});
+
+ // Clean up test data
+ $this->cleanupTestData();
});
it('creates field set with fields', function () {
diff --git a/tests/TestCase.php b/tests/TestCase.php
index ea5d77d..d3dbb97 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -5,6 +5,7 @@
use AuroraWebSoftware\FlexyField\FlexyField;
use AuroraWebSoftware\FlexyField\FlexyFieldServiceProvider;
use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\TestCase as Orchestra;
class TestCase extends Orchestra
@@ -27,13 +28,10 @@ protected function setUp(): void
// Check if we're using PostgreSQL
$isPostgreSQL = config('database.default') === 'pgsql';
- // Run migrations only once per database type
- if ($isPostgreSQL && ! static::$pgMigrated) {
+ // Run migrations if tables don't exist
+ // We check ff_schema_fields as a proxy for all tables
+ if (! Schema::hasTable('ff_schema_fields')) {
$this->runMigrations();
- static::$pgMigrated = true;
- } elseif (! $isPostgreSQL && ! static::$migrated) {
- $this->runMigrations();
- static::$migrated = true;
}
// Clean up data before each test
@@ -101,10 +99,20 @@ public function getEnvironmentSetUp($app)
protected function cleanupTestData()
{
// Clean up in the right order to avoid foreign key constraints
- \Illuminate\Support\Facades\DB::table('ff_field_values')->delete();
- \Illuminate\Support\Facades\DB::table('ff_schema_fields')->delete();
- \Illuminate\Support\Facades\DB::table('ff_schemas')->delete();
-
+ // Values first, then fields, then schemas
+ try {
+ if (Schema::hasTable('ff_field_values')) {
+ \Illuminate\Support\Facades\DB::table('ff_field_values')->delete();
+ }
+ if (Schema::hasTable('ff_schema_fields')) {
+ \Illuminate\Support\Facades\DB::table('ff_schema_fields')->delete();
+ }
+ if (Schema::hasTable('ff_schemas')) {
+ \Illuminate\Support\Facades\DB::table('ff_schemas')->delete();
+ }
+ } catch (\Exception $e) {
+ // Ignore errors during cleanup - if tables don't exist, that's fine
+ }
}
/**
@@ -117,6 +125,10 @@ protected function runMigrations()
// Run migration
$migration->up();
+
+ // Run label column migration
+ $labelMigration = include __DIR__.'/../database/migrations/add_label_column_to_schema_fields.php';
+ $labelMigration->up();
}
/**
diff --git a/tests/Unit/FieldSchemaTest.php b/tests/Unit/FieldSchemaTest.php
index e5bd056..4624f8b 100644
--- a/tests/Unit/FieldSchemaTest.php
+++ b/tests/Unit/FieldSchemaTest.php
@@ -4,11 +4,9 @@
use AuroraWebSoftware\FlexyField\Models\SchemaField;
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
- Artisan::call('migrate:fresh');
Schema::dropIfExists('ff_example_flexy_models');
Schema::create('ff_example_flexy_models', function (Blueprint $table) {
diff --git a/tests/Unit/FieldValueTest.php b/tests/Unit/FieldValueTest.php
index c9ec4b2..5cd26ea 100644
--- a/tests/Unit/FieldValueTest.php
+++ b/tests/Unit/FieldValueTest.php
@@ -3,11 +3,9 @@
use AuroraWebSoftware\FlexyField\Models\FieldSchema;
use AuroraWebSoftware\FlexyField\Models\FieldValue;
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
-use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
- Artisan::call('migrate:fresh');
Schema::dropIfExists('ff_example_flexy_models');
Schema::create('ff_example_flexy_models', function ($table) {
diff --git a/tests/Unit/FlexyFieldTest.php b/tests/Unit/FlexyFieldTest.php
index bb31479..741be81 100644
--- a/tests/Unit/FlexyFieldTest.php
+++ b/tests/Unit/FlexyFieldTest.php
@@ -4,13 +4,11 @@
use AuroraWebSoftware\FlexyField\FlexyField;
use AuroraWebSoftware\FlexyField\Tests\Concerns\CreatesSchemas;
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
-use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
uses(CreatesSchemas::class);
beforeEach(function () {
- Artisan::call('migrate:fresh');
Schema::dropIfExists('ff_example_flexy_models');
Schema::create('ff_example_flexy_models', function ($table) {
diff --git a/tests/Unit/FlexyModelTest.php b/tests/Unit/FlexyModelTest.php
index c61318b..67de276 100644
--- a/tests/Unit/FlexyModelTest.php
+++ b/tests/Unit/FlexyModelTest.php
@@ -2,13 +2,10 @@
use AuroraWebSoftware\FlexyField\Tests\Concerns\CreatesSchemas;
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
-use Illuminate\Support\Facades\Artisan;
uses(CreatesSchemas::class);
-beforeEach(function () {
- Artisan::call('migrate:fresh');
-});
+beforeEach(function () {});
it('can instantiate Flexy model', function () {
$this->createSchemaWithFields(
diff --git a/tests/Unit/SchemaFieldTest.php b/tests/Unit/SchemaFieldTest.php
index 3ec1da0..354f745 100644
--- a/tests/Unit/SchemaFieldTest.php
+++ b/tests/Unit/SchemaFieldTest.php
@@ -4,11 +4,8 @@
use AuroraWebSoftware\FlexyField\Models\FieldSchema;
use AuroraWebSoftware\FlexyField\Models\SchemaField;
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
-use Illuminate\Support\Facades\Artisan;
-beforeEach(function () {
- Artisan::call('migrate:fresh');
-});
+beforeEach(function () {});
it('can create a schema field', function () {
FieldSchema::create([
diff --git a/tests/Unit/SchemaFieldValidationTest.php b/tests/Unit/SchemaFieldValidationTest.php
index 81c50c9..f7878f7 100644
--- a/tests/Unit/SchemaFieldValidationTest.php
+++ b/tests/Unit/SchemaFieldValidationTest.php
@@ -4,11 +4,8 @@
use AuroraWebSoftware\FlexyField\Models\FieldSchema;
use AuroraWebSoftware\FlexyField\Models\SchemaField;
use AuroraWebSoftware\FlexyField\Tests\Models\ExampleFlexyModel;
-use Illuminate\Support\Facades\Artisan;
-beforeEach(function () {
- Artisan::call('migrate:fresh');
-});
+beforeEach(function () {});
it('throws exception for invalid field type', function () {
FieldSchema::create([