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([