WIP
This commit is contained in:
@@ -376,7 +376,8 @@ private function onFlushComplex(EntityManagerInterface $em, UnitOfWork $uow, ?st
|
||||
}
|
||||
|
||||
$version = $this->incrementEntityVersion($entity, $em, $uow);
|
||||
$snapshot = $pendingSnapshots[$entityId] ?? $this->snapshotEntity($entity);
|
||||
// Re-take snapshot after version increment so it captures the new version number
|
||||
$snapshot = $this->snapshotEntity($entity);
|
||||
$this->persistAuditLog($em, new AuditLog($entityType, $entityId, 'update', $diff, $snapshot, $actorProfileId, $version));
|
||||
}
|
||||
}
|
||||
@@ -513,7 +514,6 @@ protected function snapshotEntity(object $entity): array
|
||||
'prix' => $this->safeGet($entity, 'getPrix'),
|
||||
'typePiece' => $this->normalizeValue($this->safeGet($entity, 'getTypePiece')),
|
||||
'product' => $this->normalizeValue($this->safeGet($entity, 'getProduct')),
|
||||
'productIds' => $this->safeGet($entity, 'getProductIds') ?? [],
|
||||
'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs()),
|
||||
'productSlots' => $productSlots,
|
||||
'customFieldValues'=> $customFieldValues,
|
||||
@@ -691,6 +691,7 @@ use App\Repository\ProfileRepository;
|
||||
use App\Repository\SiteRepository;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class EntityVersionService
|
||||
{
|
||||
@@ -711,6 +712,7 @@ final class EntityVersionService
|
||||
public function __construct(
|
||||
private readonly AuditLogRepository $auditLogs,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly MachineRepository $machines,
|
||||
private readonly ComposantRepository $composants,
|
||||
private readonly PieceRepository $pieces,
|
||||
@@ -814,11 +816,49 @@ final class EntityVersionService
|
||||
$restoreMode = $this->checkSkeletonCompatibility($entityType, $entity, $snapshot);
|
||||
$warnings = $this->checkIntegrity($entityType, $snapshot, $restoreMode);
|
||||
|
||||
$this->applyRestore($entityType, $entity, $snapshot, $restoreMode);
|
||||
$connection = $this->em->getConnection();
|
||||
$connection->beginTransaction();
|
||||
|
||||
$this->em->flush();
|
||||
try {
|
||||
// Mark entity to skip audit subscriber (we create the AuditLog manually)
|
||||
if (method_exists($entity, 'setSkipAudit')) {
|
||||
$entity->setSkipAudit(true);
|
||||
}
|
||||
|
||||
$newVersion = method_exists($entity, 'getVersion') ? $entity->getVersion() : null;
|
||||
$this->applyRestore($entityType, $entity, $snapshot, $restoreMode);
|
||||
|
||||
// Increment version manually (since subscriber is skipped)
|
||||
if (method_exists($entity, 'incrementVersion')) {
|
||||
$entity->incrementVersion();
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$newVersion = method_exists($entity, 'getVersion') ? $entity->getVersion() : null;
|
||||
|
||||
// Create the restore AuditLog manually with action = "restore"
|
||||
$restoreAuditLog = new AuditLog(
|
||||
$entityType,
|
||||
$entityId,
|
||||
'restore',
|
||||
['restoredFromVersion' => $version, 'restoreMode' => $restoreMode],
|
||||
$this->buildCurrentSnapshot($entityType, $entity),
|
||||
$this->resolveActorProfileId(),
|
||||
$newVersion,
|
||||
);
|
||||
$this->em->persist($restoreAuditLog);
|
||||
$this->em->flush();
|
||||
|
||||
$connection->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$connection->rollBack();
|
||||
throw $e;
|
||||
} finally {
|
||||
// Clear skip flag
|
||||
if (method_exists($entity, 'setSkipAudit')) {
|
||||
$entity->setSkipAudit(false);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
@@ -872,18 +912,30 @@ final class EntityVersionService
|
||||
{
|
||||
$warnings = [];
|
||||
|
||||
// Check constructeurs
|
||||
// Check constructeurs (batch query)
|
||||
if (!empty($snapshot['constructeurIds'])) {
|
||||
$constructeurEntries = [];
|
||||
foreach ($snapshot['constructeurIds'] as $entry) {
|
||||
$id = is_array($entry) ? ($entry['id'] ?? null) : $entry;
|
||||
$name = is_array($entry) ? ($entry['name'] ?? $id) : $id;
|
||||
if ($id && null === $this->constructeurs->find($id)) {
|
||||
$warnings[] = [
|
||||
'field' => 'constructeurIds',
|
||||
'message' => sprintf("Le fournisseur '%s' n'existe plus. Il ne sera pas restauré.", $name),
|
||||
'missingEntityId' => $id,
|
||||
'missingEntityName' => $name,
|
||||
];
|
||||
if ($id) {
|
||||
$constructeurEntries[$id] = $name;
|
||||
}
|
||||
}
|
||||
if ([] !== $constructeurEntries) {
|
||||
$foundIds = array_map(
|
||||
fn ($c) => $c->getId(),
|
||||
$this->constructeurs->findBy(['id' => array_keys($constructeurEntries)]),
|
||||
);
|
||||
foreach ($constructeurEntries as $id => $name) {
|
||||
if (!in_array($id, $foundIds, true)) {
|
||||
$warnings[] = [
|
||||
'field' => 'constructeurIds',
|
||||
'message' => sprintf("Le fournisseur '%s' n'existe plus. Il ne sera pas restauré.", $name),
|
||||
'missingEntityId' => $id,
|
||||
'missingEntityName' => $name,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -906,7 +958,7 @@ final class EntityVersionService
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
// Full mode: check slot references
|
||||
// Full mode: check slot references (batch queries per slot type)
|
||||
$slotChecks = match ($entityType) {
|
||||
'composant' => [
|
||||
['key' => 'pieceSlots', 'refKey' => 'selectedPieceId', 'label' => 'pièce', 'repo' => $this->pieces],
|
||||
@@ -921,9 +973,21 @@ final class EntityVersionService
|
||||
|
||||
foreach ($slotChecks as $check) {
|
||||
$slots = $snapshot[$check['key']] ?? [];
|
||||
// Collect all referenced IDs for batch lookup
|
||||
$refIds = [];
|
||||
foreach ($slots as $i => $slot) {
|
||||
$refId = $slot[$check['refKey']] ?? null;
|
||||
if (null !== $refId && null === $check['repo']->find($refId)) {
|
||||
if (null !== $refId) {
|
||||
$refIds[$i] = $refId;
|
||||
}
|
||||
}
|
||||
if ([] === $refIds) {
|
||||
continue;
|
||||
}
|
||||
$foundEntities = $check['repo']->findBy(['id' => array_values(array_unique($refIds))]);
|
||||
$foundIds = array_map(fn ($e) => $e->getId(), $foundEntities);
|
||||
foreach ($refIds as $i => $refId) {
|
||||
if (!in_array($refId, $foundIds, true)) {
|
||||
$warnings[] = [
|
||||
'field' => sprintf('%s[%d].%s', $check['key'], $i, $check['refKey']),
|
||||
'message' => sprintf("Le %s sélectionné dans le slot n'existe plus. Le slot sera restauré vide.", $check['label']),
|
||||
@@ -1074,6 +1138,9 @@ final class EntityVersionService
|
||||
$this->em->remove($slot);
|
||||
}
|
||||
|
||||
// Flush removals first to avoid FK constraint conflicts with new slots
|
||||
$this->em->flush();
|
||||
|
||||
// Recreate piece slots
|
||||
foreach ($snapshot['pieceSlots'] ?? [] as $slotData) {
|
||||
$slot = new ComposantPieceSlot();
|
||||
@@ -1136,6 +1203,9 @@ final class EntityVersionService
|
||||
$this->em->remove($slot);
|
||||
}
|
||||
|
||||
// Flush removals first to avoid FK constraint conflicts
|
||||
$this->em->flush();
|
||||
|
||||
foreach ($snapshot['productSlots'] ?? [] as $slotData) {
|
||||
$slot = new PieceProductSlot();
|
||||
$slot->setPiece($entity);
|
||||
@@ -1161,18 +1231,160 @@ final class EntityVersionService
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a map of current CFVs by fieldId for lookup
|
||||
$currentCfvsByFieldId = [];
|
||||
if (method_exists($entity, 'getCustomFieldValues')) {
|
||||
foreach ($entity->getCustomFieldValues() as $cfv) {
|
||||
$fieldId = $cfv->getCustomField()?->getId();
|
||||
if (null !== $fieldId) {
|
||||
$currentCfvsByFieldId[$fieldId] = $cfv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($snapshotCfvs as $cfvData) {
|
||||
$cfvId = $cfvData['id'] ?? null;
|
||||
if (!$cfvId) {
|
||||
$fieldId = $cfvData['fieldId'] ?? null;
|
||||
if (!$fieldId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cfv = $this->customFieldValues->find($cfvId);
|
||||
// Try to find the current CFV by fieldId (resilient to ID changes after sync)
|
||||
$cfv = $currentCfvsByFieldId[$fieldId] ?? null;
|
||||
|
||||
// Fallback: try by original ID if fieldId lookup failed
|
||||
if (null === $cfv && !empty($cfvData['id'])) {
|
||||
$cfv = $this->customFieldValues->find($cfvData['id']);
|
||||
}
|
||||
|
||||
if (null !== $cfv) {
|
||||
$cfv->setValue($cfvData['value'] ?? null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a complete snapshot of the entity in its current state (after restore).
|
||||
* Must be consistent with the snapshots built by the audit subscribers,
|
||||
* so that a future restore from a "restore" version works correctly.
|
||||
*/
|
||||
private function buildCurrentSnapshot(string $entityType, object $entity): array
|
||||
{
|
||||
$snapshot = ['id' => $entity->getId()];
|
||||
|
||||
// Base fields
|
||||
$baseFields = self::BASE_FIELDS[$entityType] ?? [];
|
||||
foreach ($baseFields as $field) {
|
||||
$getter = 'get'.ucfirst($field);
|
||||
if (method_exists($entity, $getter)) {
|
||||
$snapshot[$field] = $entity->{$getter}();
|
||||
}
|
||||
}
|
||||
|
||||
// Version
|
||||
if (method_exists($entity, 'getVersion')) {
|
||||
$snapshot['version'] = $entity->getVersion();
|
||||
}
|
||||
|
||||
// Constructeurs
|
||||
if (method_exists($entity, 'getConstructeurs')) {
|
||||
$snapshot['constructeurIds'] = [];
|
||||
foreach ($entity->getConstructeurs() as $c) {
|
||||
$snapshot['constructeurIds'][] = ['id' => $c->getId(), 'name' => $c->getName()];
|
||||
}
|
||||
}
|
||||
|
||||
// Type (ModelType reference)
|
||||
$typeGetters = ['composant' => 'getTypeComposant', 'piece' => 'getTypePiece', 'product' => 'getTypeProduct'];
|
||||
$typeKeys = ['composant' => 'typeComposant', 'piece' => 'typePiece', 'product' => 'typeProduct'];
|
||||
if (isset($typeGetters[$entityType]) && method_exists($entity, $typeGetters[$entityType])) {
|
||||
$type = $entity->{$typeGetters[$entityType]}();
|
||||
$snapshot[$typeKeys[$entityType]] = $type ? ['id' => $type->getId(), 'name' => $type->getName(), 'code' => $type->getCode()] : null;
|
||||
}
|
||||
|
||||
// Machine: site
|
||||
if ('machine' === $entityType && method_exists($entity, 'getSite')) {
|
||||
$site = $entity->getSite();
|
||||
$snapshot['site'] = $site ? ['id' => $site->getId(), 'name' => $site->getName()] : null;
|
||||
}
|
||||
|
||||
// Composant/Piece: product
|
||||
if (in_array($entityType, ['composant', 'piece'], true) && method_exists($entity, 'getProduct')) {
|
||||
$product = $entity->getProduct();
|
||||
$snapshot['product'] = $product ? ['id' => $product->getId(), 'name' => $product->getName()] : null;
|
||||
}
|
||||
|
||||
// Slots
|
||||
if ('composant' === $entityType) {
|
||||
$snapshot['pieceSlots'] = [];
|
||||
foreach ($entity->getPieceSlots() as $slot) {
|
||||
$snapshot['pieceSlots'][] = [
|
||||
'id' => $slot->getId(), 'typePieceId' => $slot->getTypePiece()?->getId(),
|
||||
'selectedPieceId' => $slot->getSelectedPiece()?->getId(),
|
||||
'quantity' => $slot->getQuantity(), 'position' => $slot->getPosition(),
|
||||
];
|
||||
}
|
||||
$snapshot['subcomponentSlots'] = [];
|
||||
foreach ($entity->getSubcomponentSlots() as $slot) {
|
||||
$snapshot['subcomponentSlots'][] = [
|
||||
'id' => $slot->getId(), 'alias' => $slot->getAlias(), 'familyCode' => $slot->getFamilyCode(),
|
||||
'typeComposantId' => $slot->getTypeComposant()?->getId(),
|
||||
'selectedComposantId' => $slot->getSelectedComposant()?->getId(),
|
||||
'position' => $slot->getPosition(),
|
||||
];
|
||||
}
|
||||
$snapshot['productSlots'] = [];
|
||||
foreach ($entity->getProductSlots() as $slot) {
|
||||
$snapshot['productSlots'][] = [
|
||||
'id' => $slot->getId(), 'typeProductId' => $slot->getTypeProduct()?->getId(),
|
||||
'selectedProductId' => $slot->getSelectedProduct()?->getId(),
|
||||
'familyCode' => $slot->getFamilyCode(), 'position' => $slot->getPosition(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ('piece' === $entityType) {
|
||||
$snapshot['productSlots'] = [];
|
||||
foreach ($entity->getProductSlots() as $slot) {
|
||||
$snapshot['productSlots'][] = [
|
||||
'id' => $slot->getId(), 'typeProductId' => $slot->getTypeProduct()?->getId(),
|
||||
'selectedProductId' => $slot->getSelectedProduct()?->getId(),
|
||||
'familyCode' => $slot->getFamilyCode(), 'position' => $slot->getPosition(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Custom field values
|
||||
if (method_exists($entity, 'getCustomFieldValues')) {
|
||||
$snapshot['customFieldValues'] = [];
|
||||
foreach ($entity->getCustomFieldValues() as $cfv) {
|
||||
$snapshot['customFieldValues'][] = [
|
||||
'id' => $cfv->getId(), 'fieldName' => $cfv->getCustomField()?->getName(),
|
||||
'fieldId' => $cfv->getCustomField()?->getId(), 'value' => $cfv->getValue(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current actor profile ID from the session.
|
||||
* Mirrors AbstractAuditSubscriber::resolveActorProfileId().
|
||||
*/
|
||||
private function resolveActorProfileId(): ?string
|
||||
{
|
||||
try {
|
||||
$session = $this->requestStack->getSession();
|
||||
$profileId = $session->get('profileId');
|
||||
if ($profileId) {
|
||||
return (string) $profileId;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// No session available (CLI context, etc.)
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1189,6 +1401,91 @@ git commit -m "feat(versioning) : add EntityVersionService with restore logic"
|
||||
|
||||
---
|
||||
|
||||
## Task 7b: skipAudit flag on entities + subscriber check
|
||||
|
||||
The `restore()` method creates its own AuditLog with `action = "restore"`. The audit subscribers must skip entities flagged with `skipAudit = true` to avoid a duplicate `update` AuditLog.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Entity/Machine.php`, `src/Entity/Composant.php`, `src/Entity/Piece.php`, `src/Entity/Product.php`
|
||||
- Modify: `src/EventSubscriber/AbstractAuditSubscriber.php`
|
||||
|
||||
- [ ] **Step 1: Add skipAudit flag to each entity**
|
||||
|
||||
Add to Machine, Composant, Piece, Product (transient property, NOT mapped to DB):
|
||||
|
||||
```php
|
||||
/**
|
||||
* Transient flag — when true, audit subscribers skip this entity.
|
||||
* Used by EntityVersionService::restore() to avoid duplicate AuditLogs.
|
||||
*/
|
||||
private bool $skipAudit = false;
|
||||
|
||||
public function getSkipAudit(): bool
|
||||
{
|
||||
return $this->skipAudit;
|
||||
}
|
||||
|
||||
public function setSkipAudit(bool $skipAudit): static
|
||||
{
|
||||
$this->skipAudit = $skipAudit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add skipAudit check in AbstractAuditSubscriber**
|
||||
|
||||
In `onFlush()` method, add an early return that scans all scheduled entities for the `skipAudit` flag. This covers ALL paths (simple, complex, collections, CFV changes) and avoids any duplicate AuditLogs:
|
||||
|
||||
```php
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uow = $em->getUnitOfWork();
|
||||
|
||||
// If any tracked entity has skipAudit=true, skip the entire subscriber.
|
||||
// This is set by EntityVersionService::restore() to avoid duplicate audit logs.
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if ($this->supports($entity) && method_exists($entity, 'getSkipAudit') && $entity->getSkipAudit()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$actorProfileId = $this->resolveActorProfileId();
|
||||
$entityType = $this->entityType();
|
||||
|
||||
if ($this->hasCollectionTracking()) {
|
||||
$this->onFlushComplex($em, $uow, $actorProfileId, $entityType);
|
||||
} else {
|
||||
$this->onFlushSimple($em, $uow, $actorProfileId, $entityType);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This replaces the existing `onFlush()` method. The check is at the top level so it covers entity updates, collection changes, and custom field value changes — all paths that `onFlushComplex` processes.
|
||||
|
||||
- [ ] **Step 3: Run php-cs-fixer**
|
||||
|
||||
Run: `make php-cs-fixer-allow-risky`
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `make test`
|
||||
Expected: All tests pass.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Entity/Machine.php src/Entity/Composant.php src/Entity/Piece.php src/Entity/Product.php src/EventSubscriber/AbstractAuditSubscriber.php
|
||||
git commit -m "feat(versioning) : add skipAudit flag for restore-originated flushes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: EntityVersionController — REST endpoints
|
||||
|
||||
**Files:**
|
||||
@@ -1570,6 +1867,35 @@ git commit -m "test(versioning) : add EntityVersionTest for list, preview and re
|
||||
|
||||
---
|
||||
|
||||
## Task 9b: Frontend — add `restore` action label to historyDisplayUtils
|
||||
|
||||
**Files:**
|
||||
- Modify: `Inventory_frontend/app/shared/utils/historyDisplayUtils.ts`
|
||||
|
||||
- [ ] **Step 1: Add `restore` case to `historyActionLabel`**
|
||||
|
||||
In `historyDisplayUtils.ts`, update `historyActionLabel`:
|
||||
|
||||
```typescript
|
||||
export const historyActionLabel = (action: string): string => {
|
||||
if (action === 'create') return 'Création'
|
||||
if (action === 'delete') return 'Suppression'
|
||||
if (action === 'restore') return 'Restauration'
|
||||
return 'Modification'
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit in frontend repo**
|
||||
|
||||
```bash
|
||||
cd Inventory_frontend
|
||||
git add app/shared/utils/historyDisplayUtils.ts
|
||||
git commit -m "feat(versioning) : add restore action label to historyDisplayUtils"
|
||||
cd ..
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Frontend — useEntityVersions composable
|
||||
|
||||
**Files:**
|
||||
@@ -1578,7 +1904,7 @@ git commit -m "test(versioning) : add EntityVersionTest for list, preview and re
|
||||
- [ ] **Step 1: Create the composable**
|
||||
|
||||
```typescript
|
||||
import { ref } from 'vue'
|
||||
import { ref, toValue } from 'vue'
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { MaybeRef } from 'vue'
|
||||
|
||||
@@ -1631,8 +1957,8 @@ export function useEntityVersions(deps: Deps) {
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const getPath = () => {
|
||||
const type = typeof deps.entityType === 'string' ? deps.entityType : deps.entityType.value
|
||||
const id = typeof deps.entityId === 'string' ? deps.entityId : deps.entityId.value
|
||||
const type = toValue(deps.entityType)
|
||||
const id = toValue(deps.entityId)
|
||||
const base = ENTITY_ENDPOINTS[type]
|
||||
return `${base}/${id}`
|
||||
}
|
||||
@@ -1665,7 +1991,7 @@ export function useEntityVersions(deps: Deps) {
|
||||
}
|
||||
|
||||
const restore = async (version: number): Promise<RestoreResult | null> => {
|
||||
const result = await post<RestoreResult>(`${getPath()}/versions/${version}/restore`)
|
||||
const result = await post<RestoreResult>(`${getPath()}/versions/${version}/restore`, {})
|
||||
if (!result.success || !result.data) {
|
||||
return null
|
||||
}
|
||||
@@ -1952,10 +2278,13 @@ const confirmRestore = async () => {
|
||||
restoring.value = true
|
||||
const result = await restore(targetVersion.value)
|
||||
restoring.value = false
|
||||
modalVisible.value = false
|
||||
if (result?.success) {
|
||||
modalVisible.value = false
|
||||
await fetchVersions()
|
||||
emit('restored')
|
||||
} else {
|
||||
error.value = 'La restauration a échoué.'
|
||||
modalVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2017,7 +2346,7 @@ In `component/[id]/edit.vue`, add after the `EntityHistorySection` block (after
|
||||
entity-type="composant"
|
||||
:entity-id="String(route.params.id)"
|
||||
:field-labels="historyFieldLabels"
|
||||
@restored="window.location.reload()"
|
||||
@restored="fetchComponent()"
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -2046,9 +2375,26 @@ Add the import:
|
||||
import EntityVersionList from '~/components/common/EntityVersionList.vue'
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Read and modify product/[id]/edit.vue**
|
||||
- [ ] **Step 4: Add EntityVersionList to product/[id]/edit.vue**
|
||||
|
||||
Read the file first, then add `EntityVersionList` following the same pattern as the other pages.
|
||||
In `product/[id]/edit.vue`, add after the `EntityHistorySection` block and before the save buttons:
|
||||
|
||||
```vue
|
||||
<EntityVersionList
|
||||
entity-type="product"
|
||||
:entity-id="String(route.params.id)"
|
||||
:field-labels="historyFieldLabels"
|
||||
@restored="loadProduct()"
|
||||
/>
|
||||
```
|
||||
|
||||
Where `loadProduct` is the existing function that calls `getProduct(id)` and populates the form. If no such function is exposed, extract the onMounted data-loading logic into a named function that can be called from `@restored`.
|
||||
|
||||
Add the import:
|
||||
|
||||
```js
|
||||
import EntityVersionList from '~/components/common/EntityVersionList.vue'
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run lint**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user