feat(sync) : add slot selection controllers, custom field sync, and position fallbacks
- Add selectedPieceId support to ComposantPieceSlotController - Create ComposantProductSlotController and ComposantSubcomponentSlotController - Add updateCustomFields() to SkeletonStructureService for managing CustomField entities - Fix position/orderIndex fallback to array index in all 3 sync strategies - Fix type comparison in ProductSyncStrategy for dual format support - Update CLAUDE.md with new entities, controllers, and fixtures documentation - Update frontend submodule with interactive slot selectors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\SkeletonPieceRequirement;
|
||||
use App\Entity\SkeletonProductRequirement;
|
||||
use App\Entity\SkeletonSubcomponentRequirement;
|
||||
use App\Enum\ModelCategory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class SkeletonStructureService
|
||||
@@ -60,5 +62,118 @@ class SkeletonStructureService
|
||||
$req->setPosition($i);
|
||||
$modelType->addSkeletonSubcomponentRequirement($req);
|
||||
}
|
||||
|
||||
// Update custom field definitions
|
||||
$this->updateCustomFields($modelType, $structure['customFields'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync CustomField entities for this ModelType.
|
||||
* Handles two frontend formats:
|
||||
* - COMPONENT: {key, value: {type, required, options?, defaultValue?}, id?, customFieldId?}
|
||||
* - PIECE/PRODUCT: {name, type, required, options?, orderIndex?, defaultValue?}.
|
||||
*/
|
||||
private function updateCustomFields(ModelType $modelType, array $proposedFields): void
|
||||
{
|
||||
// Determine which FK to use based on category
|
||||
$category = $modelType->getCategory();
|
||||
$fkField = match ($category) {
|
||||
ModelCategory::COMPONENT => 'typeComposant',
|
||||
ModelCategory::PIECE => 'typePiece',
|
||||
ModelCategory::PRODUCT => 'typeProduct',
|
||||
};
|
||||
|
||||
// Load existing custom fields
|
||||
$existingFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
[$fkField => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
|
||||
// Index existing by ID for matching
|
||||
$existingById = [];
|
||||
foreach ($existingFields as $cf) {
|
||||
$existingById[$cf->getId()] = $cf;
|
||||
}
|
||||
|
||||
$processedIds = [];
|
||||
|
||||
foreach ($proposedFields as $i => $fieldData) {
|
||||
// Normalize both formats to a common shape
|
||||
$normalized = $this->normalizeCustomFieldData($fieldData, $i);
|
||||
|
||||
// Try to match an existing field by ID
|
||||
$existingField = null;
|
||||
$fieldId = $fieldData['customFieldId'] ?? $fieldData['id'] ?? null;
|
||||
if ($fieldId && isset($existingById[$fieldId])) {
|
||||
$existingField = $existingById[$fieldId];
|
||||
}
|
||||
|
||||
if ($existingField) {
|
||||
// Update existing field
|
||||
$existingField->setName($normalized['name']);
|
||||
$existingField->setType($normalized['type']);
|
||||
$existingField->setRequired($normalized['required']);
|
||||
$existingField->setOptions($normalized['options']);
|
||||
$existingField->setDefaultValue($normalized['defaultValue']);
|
||||
$existingField->setOrderIndex($normalized['orderIndex']);
|
||||
$processedIds[$existingField->getId()] = true;
|
||||
} else {
|
||||
// Create new field
|
||||
$cf = new CustomField();
|
||||
$cf->setName($normalized['name']);
|
||||
$cf->setType($normalized['type']);
|
||||
$cf->setRequired($normalized['required']);
|
||||
$cf->setOptions($normalized['options']);
|
||||
$cf->setDefaultValue($normalized['defaultValue']);
|
||||
$cf->setOrderIndex($normalized['orderIndex']);
|
||||
|
||||
match ($category) {
|
||||
ModelCategory::COMPONENT => $cf->setTypeComposant($modelType),
|
||||
ModelCategory::PIECE => $cf->setTypePiece($modelType),
|
||||
ModelCategory::PRODUCT => $cf->setTypeProduct($modelType),
|
||||
};
|
||||
|
||||
$this->em->persist($cf);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove orphaned fields (exist in DB but not in proposed)
|
||||
foreach ($existingFields as $cf) {
|
||||
if (!isset($processedIds[$cf->getId()])) {
|
||||
$this->em->remove($cf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize frontend custom field data to a common shape.
|
||||
*
|
||||
* @return array{name: string, type: string, required: bool, options: ?array, defaultValue: ?string, orderIndex: int}
|
||||
*/
|
||||
private function normalizeCustomFieldData(array $fieldData, int $index): array
|
||||
{
|
||||
// COMPONENT format: {key: "name", value: {type, required, options?, defaultValue?}}
|
||||
if (isset($fieldData['key'], $fieldData['value'])) {
|
||||
$value = $fieldData['value'];
|
||||
|
||||
return [
|
||||
'name' => $fieldData['key'],
|
||||
'type' => $value['type'] ?? 'text',
|
||||
'required' => (bool) ($value['required'] ?? false),
|
||||
'options' => $value['options'] ?? null,
|
||||
'defaultValue' => $value['defaultValue'] ?? null,
|
||||
'orderIndex' => $index,
|
||||
];
|
||||
}
|
||||
|
||||
// PIECE/PRODUCT format: {name, type, required, options?, orderIndex?, defaultValue?}
|
||||
return [
|
||||
'name' => $fieldData['name'] ?? '',
|
||||
'type' => $fieldData['type'] ?? 'text',
|
||||
'required' => (bool) ($fieldData['required'] ?? false),
|
||||
'options' => $fieldData['options'] ?? null,
|
||||
'defaultValue' => $fieldData['defaultValue'] ?? null,
|
||||
'orderIndex' => $fieldData['orderIndex'] ?? $index,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user