fix(sync) : preserve slot selections when modifying ModelType structure
SkeletonStructureService was deleting all skeleton requirements and recreating them on every ModelType update. Combined with position-based matching in sync strategies, any reordering or insertion caused all existing slots to be orphaned and recreated empty, losing selections. - SkeletonStructureService: update requirements in-place by matching on typeId instead of delete-all/recreate-all - ComposantSyncStrategy & PieceSyncStrategy: two-pass smart matching algorithm (exact typeId+position first, then typeId-only fallback) to preserve selectedPiece/selectedComposant/selectedProduct on reorder/insertion - Frontend: check patch result.success before updating local state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,55 +18,158 @@ class SkeletonStructureService
|
||||
|
||||
public function updateSkeletonRequirements(ModelType $modelType, array $structure): void
|
||||
{
|
||||
// Clear existing requirements
|
||||
foreach ($modelType->getSkeletonPieceRequirements() as $req) {
|
||||
$modelType->removeSkeletonPieceRequirement($req);
|
||||
}
|
||||
// Update piece requirements in-place (match by typeId, then update position)
|
||||
$this->syncPieceRequirements($modelType, $structure['pieces'] ?? []);
|
||||
|
||||
foreach ($modelType->getSkeletonProductRequirements() as $req) {
|
||||
$modelType->removeSkeletonProductRequirement($req);
|
||||
}
|
||||
// Update product requirements in-place
|
||||
$this->syncProductRequirements($modelType, $structure['products'] ?? []);
|
||||
|
||||
foreach ($modelType->getSkeletonSubcomponentRequirements() as $req) {
|
||||
$modelType->removeSkeletonSubcomponentRequirement($req);
|
||||
}
|
||||
|
||||
// Create piece requirements
|
||||
foreach (($structure['pieces'] ?? []) as $i => $pieceData) {
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setTypePiece($this->em->getReference(ModelType::class, $pieceData['typePieceId']));
|
||||
$req->setPosition($i);
|
||||
$modelType->addSkeletonPieceRequirement($req);
|
||||
}
|
||||
|
||||
// Create product requirements (shared by component + piece types)
|
||||
foreach (($structure['products'] ?? []) as $i => $prodData) {
|
||||
$req = new SkeletonProductRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setTypeProduct($this->em->getReference(ModelType::class, $prodData['typeProductId']));
|
||||
$req->setFamilyCode($prodData['familyCode'] ?? null);
|
||||
$req->setPosition($i);
|
||||
$modelType->addSkeletonProductRequirement($req);
|
||||
}
|
||||
|
||||
// Create subcomponent requirements (component types only)
|
||||
foreach (($structure['subcomponents'] ?? []) as $i => $subData) {
|
||||
$req = new SkeletonSubcomponentRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setAlias($subData['alias'] ?? '');
|
||||
$req->setFamilyCode($subData['familyCode'] ?? '');
|
||||
if (!empty($subData['typeComposantId'])) {
|
||||
$req->setTypeComposant($this->em->getReference(ModelType::class, $subData['typeComposantId']));
|
||||
}
|
||||
$req->setPosition($i);
|
||||
$modelType->addSkeletonSubcomponentRequirement($req);
|
||||
}
|
||||
// Update subcomponent requirements in-place
|
||||
$this->syncSubcomponentRequirements($modelType, $structure['subcomponents'] ?? []);
|
||||
|
||||
// Update custom field definitions
|
||||
$this->updateCustomFields($modelType, $structure['customFields'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{typePieceId: string}> $proposedPieces
|
||||
*/
|
||||
private function syncPieceRequirements(ModelType $modelType, array $proposedPieces): void
|
||||
{
|
||||
$existing = $modelType->getSkeletonPieceRequirements()->toArray();
|
||||
|
||||
// Index existing by typeId for matching
|
||||
$existingByTypeId = [];
|
||||
foreach ($existing as $req) {
|
||||
$existingByTypeId[$req->getTypePiece()->getId()][] = $req;
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
$toCreate = [];
|
||||
|
||||
foreach ($proposedPieces as $i => $pieceData) {
|
||||
$typeId = $pieceData['typePieceId'];
|
||||
if (!empty($existingByTypeId[$typeId])) {
|
||||
// Reuse existing requirement, update position
|
||||
$req = array_shift($existingByTypeId[$typeId]);
|
||||
$req->setPosition($i);
|
||||
$matched[spl_object_id($req)] = true;
|
||||
} else {
|
||||
$toCreate[] = ['data' => $pieceData, 'position' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unmatched existing requirements
|
||||
foreach ($existing as $req) {
|
||||
if (!isset($matched[spl_object_id($req)])) {
|
||||
$modelType->removeSkeletonPieceRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new requirements
|
||||
foreach ($toCreate as $item) {
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setTypePiece($this->em->getReference(ModelType::class, $item['data']['typePieceId']));
|
||||
$req->setPosition($item['position']);
|
||||
$modelType->addSkeletonPieceRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{typeProductId: string, familyCode?: ?string}> $proposedProducts
|
||||
*/
|
||||
private function syncProductRequirements(ModelType $modelType, array $proposedProducts): void
|
||||
{
|
||||
$existing = $modelType->getSkeletonProductRequirements()->toArray();
|
||||
|
||||
$existingByTypeId = [];
|
||||
foreach ($existing as $req) {
|
||||
$existingByTypeId[$req->getTypeProduct()->getId()][] = $req;
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
$toCreate = [];
|
||||
|
||||
foreach ($proposedProducts as $i => $prodData) {
|
||||
$typeId = $prodData['typeProductId'];
|
||||
if (!empty($existingByTypeId[$typeId])) {
|
||||
$req = array_shift($existingByTypeId[$typeId]);
|
||||
$req->setFamilyCode($prodData['familyCode'] ?? null);
|
||||
$req->setPosition($i);
|
||||
$matched[spl_object_id($req)] = true;
|
||||
} else {
|
||||
$toCreate[] = ['data' => $prodData, 'position' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($existing as $req) {
|
||||
if (!isset($matched[spl_object_id($req)])) {
|
||||
$modelType->removeSkeletonProductRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($toCreate as $item) {
|
||||
$req = new SkeletonProductRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setTypeProduct($this->em->getReference(ModelType::class, $item['data']['typeProductId']));
|
||||
$req->setFamilyCode($item['data']['familyCode'] ?? null);
|
||||
$req->setPosition($item['position']);
|
||||
$modelType->addSkeletonProductRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{alias?: string, familyCode?: string, typeComposantId?: string}> $proposedSubs
|
||||
*/
|
||||
private function syncSubcomponentRequirements(ModelType $modelType, array $proposedSubs): void
|
||||
{
|
||||
$existing = $modelType->getSkeletonSubcomponentRequirements()->toArray();
|
||||
|
||||
$existingByTypeId = [];
|
||||
foreach ($existing as $req) {
|
||||
$key = $req->getTypeComposant()?->getId() ?? '';
|
||||
$existingByTypeId[$key][] = $req;
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
$toCreate = [];
|
||||
|
||||
foreach ($proposedSubs as $i => $subData) {
|
||||
$typeId = $subData['typeComposantId'] ?? '';
|
||||
if (!empty($existingByTypeId[$typeId])) {
|
||||
$req = array_shift($existingByTypeId[$typeId]);
|
||||
$req->setAlias($subData['alias'] ?? '');
|
||||
$req->setFamilyCode($subData['familyCode'] ?? '');
|
||||
if (!empty($subData['typeComposantId'])) {
|
||||
$req->setTypeComposant($this->em->getReference(ModelType::class, $subData['typeComposantId']));
|
||||
}
|
||||
$req->setPosition($i);
|
||||
$matched[spl_object_id($req)] = true;
|
||||
} else {
|
||||
$toCreate[] = ['data' => $subData, 'position' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($existing as $req) {
|
||||
if (!isset($matched[spl_object_id($req)])) {
|
||||
$modelType->removeSkeletonSubcomponentRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($toCreate as $item) {
|
||||
$req = new SkeletonSubcomponentRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setAlias($item['data']['alias'] ?? '');
|
||||
$req->setFamilyCode($item['data']['familyCode'] ?? '');
|
||||
if (!empty($item['data']['typeComposantId'])) {
|
||||
$req->setTypeComposant($this->em->getReference(ModelType::class, $item['data']['typeComposantId']));
|
||||
}
|
||||
$req->setPosition($item['position']);
|
||||
$modelType->addSkeletonSubcomponentRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync CustomField entities for this ModelType.
|
||||
* Handles two frontend formats:
|
||||
|
||||
Reference in New Issue
Block a user