docs : update implementation plan with review fixes
Addresses 8 issues found by dual code review: - Add readOnly + optionsText to CustomFieldInput type - Replace computed with mutable ref + refresh() in composable - Add metadata fallback for fields without customFieldId - Add onValueCreated callback to keep parent reactive state in sync - Merge Task 4+5 to avoid type mismatch in intermediate state - Detail surgical refactoring of transformComponentCustomFields - Define data contract for ComponentItem/PieceItem (pre-merged) - Fix hasDisplayableValue to handle readOnly fields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,9 @@ export interface CustomFieldInput {
|
||||
orderIndex: number
|
||||
machineContextOnly: boolean
|
||||
value: string
|
||||
readOnly?: boolean
|
||||
/** options joined by newline — used by category editor textareas (v-model) */
|
||||
optionsText?: string
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -260,6 +263,8 @@ export function mergeDefinitionsWithValues(
|
||||
const result: CustomFieldInput[] = definitions.map((def) => {
|
||||
const matched = (def.id ? valueById.get(def.id) : undefined) ?? valueByName.get(def.name)
|
||||
|
||||
const optionsText = def.options.length ? def.options.join('\n') : undefined
|
||||
|
||||
if (matched) {
|
||||
if (matched.id) matchedValueIds.add(matched.id)
|
||||
matchedNames.add(def.name)
|
||||
@@ -274,6 +279,7 @@ export function mergeDefinitionsWithValues(
|
||||
orderIndex: def.orderIndex,
|
||||
machineContextOnly: def.machineContextOnly,
|
||||
value: matched.value,
|
||||
optionsText,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +295,7 @@ export function mergeDefinitionsWithValues(
|
||||
orderIndex: def.orderIndex,
|
||||
machineContextOnly: def.machineContextOnly,
|
||||
value: def.defaultValue ?? '',
|
||||
optionsText,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -297,6 +304,7 @@ export function mergeDefinitionsWithValues(
|
||||
if (matchedValueIds.has(v.id)) continue
|
||||
if (matchedNames.has(v.customField.name)) continue
|
||||
|
||||
const orphanOptionsText = v.customField.options.length ? v.customField.options.join('\n') : undefined
|
||||
result.push({
|
||||
customFieldId: v.customField.id,
|
||||
customFieldValueId: v.id || null,
|
||||
@@ -308,6 +316,7 @@ export function mergeDefinitionsWithValues(
|
||||
orderIndex: v.customField.orderIndex,
|
||||
machineContextOnly: v.customField.machineContextOnly,
|
||||
value: v.value,
|
||||
optionsText: orphanOptionsText,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -347,8 +356,9 @@ export function formatValueForDisplay(field: CustomFieldInput): string {
|
||||
return raw || 'Non défini'
|
||||
}
|
||||
|
||||
/** Whether a field has a displayable value */
|
||||
/** Whether a field has a displayable value (readOnly fields always display) */
|
||||
export function hasDisplayableValue(field: CustomFieldInput): boolean {
|
||||
if (field.readOnly) return true
|
||||
if (field.type === 'boolean') return field.value !== undefined && field.value !== null && field.value !== ''
|
||||
return typeof field.value === 'string' && field.value.trim().length > 0
|
||||
}
|
||||
@@ -413,9 +423,14 @@ This replaces `useEntityCustomFields.ts` (181 lines) and the custom field parts
|
||||
*
|
||||
* Replaces: useEntityCustomFields.ts, custom field parts of useMachineDetailCustomFields.ts,
|
||||
* and inline custom field logic in useComponentEdit/useComponentCreate/usePieceEdit.
|
||||
*
|
||||
* DESIGN NOTE: Uses an internal mutable `ref` (not a `computed`) so that
|
||||
* save operations can update `customFieldValueId` in place without being
|
||||
* overwritten on the next reactivity cycle. Call `refresh()` to re-merge
|
||||
* from the source definitions + values (e.g. after fetching fresh data).
|
||||
*/
|
||||
|
||||
import { computed, type MaybeRef, toValue } from 'vue'
|
||||
import { ref, watch, computed, type MaybeRef, toValue } from 'vue'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import {
|
||||
@@ -450,6 +465,8 @@ export interface UseCustomFieldInputsOptions {
|
||||
entityId: MaybeRef<string | null>
|
||||
/** Filter context: 'standalone' hides machineContextOnly, 'machine' shows only machineContextOnly */
|
||||
context?: 'standalone' | 'machine'
|
||||
/** Optional callback to update the source values array after a save (keeps parent reactive state in sync) */
|
||||
onValueCreated?: (newValue: { id: string; value: string; customField: any }) => void
|
||||
}
|
||||
|
||||
export function useCustomFieldInputs(options: UseCustomFieldInputsOptions) {
|
||||
@@ -460,22 +477,40 @@ export function useCustomFieldInputs(options: UseCustomFieldInputsOptions) {
|
||||
} = useCustomFields()
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
// Merged fields: definitions + values
|
||||
const allFields = computed<CustomFieldInput[]>(() => {
|
||||
// Internal mutable state — NOT a computed, so save can mutate in place
|
||||
const _allFields = ref<CustomFieldInput[]>([])
|
||||
|
||||
// Re-merge from source definitions + values
|
||||
const refresh = () => {
|
||||
const defs = toValue(options.definitions)
|
||||
const vals = toValue(options.values)
|
||||
return mergeDefinitionsWithValues(defs, vals)
|
||||
})
|
||||
_allFields.value = mergeDefinitionsWithValues(defs, vals)
|
||||
}
|
||||
|
||||
// Auto-refresh when reactive sources change
|
||||
watch(
|
||||
() => [toValue(options.definitions), toValue(options.values)],
|
||||
() => refresh(),
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
// Filtered by context (standalone vs machine)
|
||||
const fields = computed<CustomFieldInput[]>(() => {
|
||||
if (!context) return allFields.value
|
||||
return filterByContext(allFields.value, context)
|
||||
if (!context) return _allFields.value
|
||||
return filterByContext(_allFields.value, context)
|
||||
})
|
||||
|
||||
// Validation
|
||||
const requiredFilled = computed(() => requiredFieldsFilled(fields.value))
|
||||
|
||||
// Build metadata for upsert when no customFieldId is available (legacy fallback)
|
||||
const _buildMetadata = (field: CustomFieldInput) => ({
|
||||
customFieldName: field.name,
|
||||
customFieldType: field.type,
|
||||
customFieldRequired: field.required,
|
||||
customFieldOptions: field.options,
|
||||
})
|
||||
|
||||
// Update a single field value
|
||||
const update = async (field: CustomFieldInput): Promise<boolean> => {
|
||||
const id = toValue(options.entityId)
|
||||
@@ -497,24 +532,28 @@ export function useCustomFieldInputs(options: UseCustomFieldInputsOptions) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Create new value via upsert
|
||||
if (!field.customFieldId) {
|
||||
showError(`Impossible de sauvegarder le champ "${field.name}" (identifiant manquant)`)
|
||||
return false
|
||||
}
|
||||
|
||||
// Create new value via upsert — with metadata fallback when no ID
|
||||
const metadata = field.customFieldId ? undefined : _buildMetadata(field)
|
||||
const result: any = await upsertCustomFieldValue(
|
||||
field.customFieldId,
|
||||
entityType,
|
||||
id,
|
||||
value,
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
// Update field with the newly created value ID
|
||||
// Mutate in place (safe — _allFields is a ref, not computed)
|
||||
if (result.data?.id) {
|
||||
field.customFieldValueId = result.data.id
|
||||
}
|
||||
if (result.data?.customField?.id) {
|
||||
field.customFieldId = result.data.customField.id
|
||||
}
|
||||
// Notify parent to update its reactive source
|
||||
if (options.onValueCreated && result.data) {
|
||||
options.onValueCreated(result.data)
|
||||
}
|
||||
showSuccess(`Champ "${field.name}" enregistré`)
|
||||
return true
|
||||
}
|
||||
@@ -541,22 +580,26 @@ export function useCustomFieldInputs(options: UseCustomFieldInputsOptions) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!field.customFieldId) {
|
||||
failed.push(field.name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Upsert with metadata fallback when no customFieldId
|
||||
const metadata = field.customFieldId ? undefined : _buildMetadata(field)
|
||||
const result: any = await upsertCustomFieldValue(
|
||||
field.customFieldId,
|
||||
entityType,
|
||||
id,
|
||||
value,
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
if (result.data?.id) {
|
||||
field.customFieldValueId = result.data.id
|
||||
}
|
||||
if (result.data?.customField?.id) {
|
||||
field.customFieldId = result.data.customField.id
|
||||
}
|
||||
if (options.onValueCreated && result.data) {
|
||||
options.onValueCreated(result.data)
|
||||
}
|
||||
} else {
|
||||
failed.push(field.name)
|
||||
}
|
||||
@@ -569,13 +612,15 @@ export function useCustomFieldInputs(options: UseCustomFieldInputsOptions) {
|
||||
/** All merged fields filtered by context */
|
||||
fields,
|
||||
/** All merged fields (unfiltered) */
|
||||
allFields,
|
||||
allFields: _allFields,
|
||||
/** Whether all required fields have values */
|
||||
requiredFilled,
|
||||
/** Update a single field value via API */
|
||||
update,
|
||||
/** Save all fields with values, returns list of failed field names */
|
||||
saveAll,
|
||||
/** Re-merge from source definitions + values (call after fetching fresh data) */
|
||||
refresh,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -595,18 +640,20 @@ git commit -m "feat(custom-fields) : add unified useCustomFieldInputs composable
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Migrate `CustomFieldInputGrid.vue` and `CustomFieldDisplay.vue`
|
||||
## Task 4: Migrate shared components + standalone composables (atomic batch)
|
||||
|
||||
These are shared components used everywhere. Migrate them first so downstream consumers can use the new types.
|
||||
**Why batched:** `CustomFieldInputGrid.vue` and `CustomFieldDisplay.vue` receive fields from the composables. Migrating them separately would cause TypeScript errors in the intermediate state. Migrate all together.
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/components/common/CustomFieldInputGrid.vue`
|
||||
- Modify: `frontend/app/components/common/CustomFieldDisplay.vue`
|
||||
- Modify: `frontend/app/composables/useComponentEdit.ts`
|
||||
- Modify: `frontend/app/composables/useComponentCreate.ts`
|
||||
- Modify: `frontend/app/composables/usePieceEdit.ts`
|
||||
|
||||
- [ ] **Step 1: Migrate `CustomFieldInputGrid.vue`**
|
||||
|
||||
Replace the import from `customFieldFormUtils`:
|
||||
|
||||
Replace the import:
|
||||
```typescript
|
||||
// OLD
|
||||
import { fieldKey, type CustomFieldInput } from '~/shared/utils/customFieldFormUtils'
|
||||
@@ -616,97 +663,48 @@ import { fieldKey, type CustomFieldInput } from '~/shared/utils/customFields'
|
||||
|
||||
- [ ] **Step 2: Migrate `CustomFieldDisplay.vue`**
|
||||
|
||||
Replace all imports from `entityCustomFieldLogic` with the new module. Read the file first to identify the exact imports, then replace:
|
||||
Replace all imports from `entityCustomFieldLogic`. With the new typed `CustomFieldInput`, direct property access (`.name`, `.type`, `.options`, `.value`, `.readOnly`) replaces the `resolveFieldXxx()` wrapper functions. Read the full file and adapt the template accordingly.
|
||||
|
||||
```typescript
|
||||
// OLD
|
||||
import {
|
||||
resolveFieldKey,
|
||||
resolveFieldName,
|
||||
resolveFieldType,
|
||||
resolveFieldOptions,
|
||||
resolveFieldReadOnly,
|
||||
formatFieldDisplayValue,
|
||||
resolveCustomFieldId,
|
||||
} from '~/shared/utils/entityCustomFieldLogic'
|
||||
- [ ] **Step 3: Migrate `useComponentEdit.ts`**
|
||||
|
||||
// NEW
|
||||
import {
|
||||
fieldKey,
|
||||
formatValueForDisplay,
|
||||
hasDisplayableValue,
|
||||
type CustomFieldInput,
|
||||
} from '~/shared/utils/customFields'
|
||||
```
|
||||
Read the file. Key changes:
|
||||
1. Replace `customFieldFormUtils` imports with the new module
|
||||
2. Replace the imperative `refreshCustomFieldInputs` + `buildCustomFieldInputs` pattern with `useCustomFieldInputs`
|
||||
3. Pass `selectedTypeStructure.value?.customFields` as definitions and `component.value?.customFieldValues` as values
|
||||
4. Use the `onValueCreated` callback to push new values into `component.value.customFieldValues` so the reactive source stays in sync
|
||||
5. Replace calls to `refreshCustomFieldInputs()` in watchers/fetch with calls to `refresh()` from the composable
|
||||
|
||||
Note: `CustomFieldDisplay.vue` accesses field properties directly (`.name`, `.type`, `.options`, `.value`). With the new typed `CustomFieldInput`, direct property access replaces the `resolveFieldXxx()` wrapper functions. Read the full file and adapt the template accordingly.
|
||||
|
||||
- [ ] **Step 3: Run lint + typecheck**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/app/components/common/CustomFieldInputGrid.vue frontend/app/components/common/CustomFieldDisplay.vue
|
||||
git commit -m "refactor(custom-fields) : migrate shared display components to unified module"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Migrate standalone composables (`useComponentEdit`, `useComponentCreate`, `usePieceEdit`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/composables/useComponentEdit.ts`
|
||||
- Modify: `frontend/app/composables/useComponentCreate.ts`
|
||||
- Modify: `frontend/app/composables/usePieceEdit.ts`
|
||||
|
||||
For each file:
|
||||
|
||||
1. Read the full file to understand the current custom field logic
|
||||
2. Replace imports from `customFieldFormUtils` with the new module
|
||||
3. Replace `buildCustomFieldInputs(structure, values)` with `mergeDefinitionsWithValues(structure?.customFields, values)`
|
||||
4. Replace `requiredCustomFieldsFilled(customFieldInputs.value)` with `requiredFieldsFilled(fields)`
|
||||
5. Replace `saveCustomFieldValues(...)` with `saveAll()` from `useCustomFieldInputs`
|
||||
6. Replace `normalizeCustomFieldInputs(structure)` with `normalizeDefinitions(structure?.customFields)`
|
||||
|
||||
- [ ] **Step 1: Migrate `useComponentEdit.ts`**
|
||||
|
||||
Read the file. Replace the `customFieldFormUtils` imports and the `refreshCustomFieldInputs` / `buildCustomFieldInputs` calls with `useCustomFieldInputs`. The composable should receive `selectedTypeStructure.value?.customFields` as definitions and `component.value?.customFieldValues` as values.
|
||||
|
||||
- [ ] **Step 2: Migrate `useComponentCreate.ts`**
|
||||
- [ ] **Step 4: Migrate `useComponentCreate.ts`**
|
||||
|
||||
Same pattern. Definitions come from `selectedType.value?.structure?.customFields`, values are empty `[]` for creation.
|
||||
|
||||
- [ ] **Step 3: Migrate `usePieceEdit.ts`**
|
||||
- [ ] **Step 5: Migrate `usePieceEdit.ts`**
|
||||
|
||||
Same pattern. Definitions come from the piece type structure, values from `piece.customFieldValues`.
|
||||
Same pattern. Definitions from piece type structure, values from `piece.customFieldValues`.
|
||||
|
||||
- [ ] **Step 4: Run lint + typecheck**
|
||||
- [ ] **Step 6: Run lint + typecheck**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify**
|
||||
- [ ] **Step 7: Verify**
|
||||
|
||||
Open each page in the browser:
|
||||
- `/component/{id}` — check custom fields display and edit
|
||||
- `/component/create` — check custom fields with default values
|
||||
- `/pieces/{id}/edit` — check custom fields display and edit
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/app/composables/useComponentEdit.ts frontend/app/composables/useComponentCreate.ts frontend/app/composables/usePieceEdit.ts
|
||||
git commit -m "refactor(custom-fields) : migrate standalone composables to unified module"
|
||||
git add frontend/app/components/common/CustomFieldInputGrid.vue frontend/app/components/common/CustomFieldDisplay.vue frontend/app/composables/useComponentEdit.ts frontend/app/composables/useComponentCreate.ts frontend/app/composables/usePieceEdit.ts
|
||||
git commit -m "refactor(custom-fields) : migrate shared components and standalone composables to unified module"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Migrate standalone pages (product + piece create)
|
||||
## Task 5: Migrate standalone pages (product + piece create)
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/pages/pieces/create.vue`
|
||||
@@ -755,7 +753,7 @@ git commit -m "refactor(custom-fields) : migrate product and piece pages to unif
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Migrate machine page — `MachineCustomFieldsCard`, `MachineInfoCard`, `useMachineDetailCustomFields`
|
||||
## Task 6: Migrate machine page — `MachineCustomFieldsCard`, `MachineInfoCard`, `useMachineDetailCustomFields`
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/components/machine/MachineCustomFieldsCard.vue`
|
||||
@@ -775,17 +773,43 @@ import { formatValueForDisplay, type CustomFieldInput } from '~/shared/utils/cus
|
||||
|
||||
Replace all calls to `formatCustomFieldValue(field)` with `formatValueForDisplay(field)`.
|
||||
|
||||
- [ ] **Step 2: Migrate `useMachineDetailCustomFields.ts`**
|
||||
- [ ] **Step 2: Migrate `useMachineDetailCustomFields.ts` — pure custom field functions**
|
||||
|
||||
This is the biggest migration. Read the full file. The custom field logic needs to be replaced:
|
||||
Replace the following pure-CF functions (~168 lines) with the new module:
|
||||
|
||||
1. Replace imports from `customFieldUtils` with the new module
|
||||
2. Replace `syncMachineCustomFields()` — use `mergeDefinitionsWithValues(machine.customFields, machine.customFieldValues)`
|
||||
3. Replace `transformComponentCustomFields()` and `transformCustomFields()` — for each component/piece in the hierarchy, use `mergeDefinitionsWithValues(type.customFields, entity.customFieldValues)` then `filterByContext(fields, 'standalone')`
|
||||
4. For context fields on links: `mergeDefinitionsWithValues(link.contextCustomFields, link.contextCustomFieldValues)`
|
||||
5. Replace `updateMachineCustomField()`, `saveAllMachineCustomFields()`, `saveAllContextCustomFields()` with `useCustomFieldInputs` instances or direct API calls from `useCustomFields`
|
||||
| Old function (lines) | Replacement |
|
||||
|---|---|
|
||||
| `syncMachineCustomFields` (269-289) | `mergeDefinitionsWithValues(machine.customFields, machine.customFieldValues)` |
|
||||
| `setMachineCustomFieldValue` (291-299) | Direct property mutation on the mutable `CustomFieldInput` |
|
||||
| `updateMachineCustomField` (302-363) | `useCustomFieldInputs.update()` |
|
||||
| `saveAllMachineCustomFields` (461-511) | `useCustomFieldInputs.saveAll()` |
|
||||
| `saveAllContextCustomFields` (430-459) | Loop over link-level `useCustomFieldInputs` instances |
|
||||
|
||||
Keep the non-custom-field logic (constructeurs, products, transforms) in this file.
|
||||
- [ ] **Step 3: Migrate `useMachineDetailCustomFields.ts` — mixed transform functions**
|
||||
|
||||
`transformCustomFields` (lines 71-158) and `transformComponentCustomFields` (lines 161-263) mix custom field merging with constructeur/product/document logic in a single `map()`. Refactor surgically:
|
||||
|
||||
**Inside `transformCustomFields` map callback**, replace lines 82-106 (valueEntries + merge + dedupe + filter):
|
||||
```typescript
|
||||
// OLD: ~25 lines of valueEntries building, normalizeCustomFieldValueEntry, mergeCustomFieldValuesWithDefinitions, dedupeCustomFieldEntries
|
||||
// NEW: 2 lines
|
||||
const customFields = filterByContext(
|
||||
mergeDefinitionsWithValues(type.customFields ?? typePiece.customFields ?? [], piece.customFieldValues ?? []),
|
||||
'standalone',
|
||||
)
|
||||
```
|
||||
|
||||
Keep the rest of the map callback (constructeurs lines 108-133, product lines 119-120, assembly lines 135-158) unchanged.
|
||||
|
||||
**Inside `transformComponentCustomFields` map callback**, replace lines 175-199 (same pattern):
|
||||
```typescript
|
||||
const customFields = filterByContext(
|
||||
mergeDefinitionsWithValues(type.customFields ?? [], component.customFieldValues ?? actualComponent?.customFieldValues ?? []),
|
||||
'standalone',
|
||||
)
|
||||
```
|
||||
|
||||
Keep recursive calls (lines 201-209) and constructeur/product/document logic (lines 212-263) unchanged.
|
||||
|
||||
- [ ] **Step 3: Run lint + typecheck**
|
||||
|
||||
@@ -810,7 +834,9 @@ git commit -m "refactor(custom-fields) : migrate machine page to unified module"
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Migrate hierarchy components (`ComponentItem.vue`, `PieceItem.vue`)
|
||||
## Task 7: Migrate hierarchy components (`ComponentItem.vue`, `PieceItem.vue`)
|
||||
|
||||
**Data contract decision:** The parent (`useMachineDetailCustomFields.transformComponentCustomFields`) already pre-merges custom fields into `component.customFields` (a `CustomFieldInput[]`). The Item components should NOT re-merge — they display the pre-merged data directly and use the API composable only for saves/updates.
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/components/ComponentItem.vue`
|
||||
@@ -825,17 +851,21 @@ import { resolveFieldKey, resolveFieldName, ... } from '~/shared/utils/entityCus
|
||||
```
|
||||
With:
|
||||
```typescript
|
||||
import { useCustomFieldInputs } from '~/composables/useCustomFieldInputs'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { fieldKey, formatValueForDisplay, hasDisplayableValue, type CustomFieldInput } from '~/shared/utils/customFields'
|
||||
```
|
||||
|
||||
Replace `useEntityCustomFields({ entity: () => props.component, entityType: 'composant' })` with `useCustomFieldInputs(...)`, passing the type's customFields as definitions and the entity's customFieldValues as values.
|
||||
|
||||
Update the template to use the new field properties directly instead of `resolveFieldXxx()` functions.
|
||||
Key changes:
|
||||
1. **Remove `useEntityCustomFields`** — the parent already pre-merges. Use `props.component.customFields` directly (already `CustomFieldInput[]` after Task 6)
|
||||
2. **For display:** use `hasDisplayableValue(field)` and `formatValueForDisplay(field)` instead of `resolveFieldXxx()` wrappers
|
||||
3. **For edits/saves:** use `useCustomFields()` directly (the HTTP layer) instead of `useEntityCustomFields().updateCustomField`
|
||||
4. **For context fields** (`component.contextCustomFields` + `component.contextCustomFieldValues`): merge locally with `mergeDefinitionsWithValues` — these are NOT pre-merged by the parent since they come as separate arrays
|
||||
5. **Replace `resolveCustomFieldId(field)`** with `field.customFieldId` (direct property access on `CustomFieldInput`)
|
||||
6. **Replace `resolveFieldId(field)`** with `field.customFieldValueId`
|
||||
|
||||
- [ ] **Step 2: Migrate `PieceItem.vue`**
|
||||
|
||||
Same pattern as ComponentItem but with `entityType: 'piece'` and piece type fields.
|
||||
Same pattern as ComponentItem but with `entityType: 'piece'` and `props.piece.customFields`.
|
||||
|
||||
- [ ] **Step 3: Run lint + typecheck**
|
||||
|
||||
@@ -856,7 +886,7 @@ git commit -m "refactor(custom-fields) : migrate ComponentItem and PieceItem to
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Clean category editor files (`componentStructure*.ts`)
|
||||
## Task 8: Clean category editor files (`componentStructure*.ts`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/shared/model/componentStructure.ts`
|
||||
@@ -875,11 +905,13 @@ In `componentStructure.ts`, replace the custom field handling in `normalizeStruc
|
||||
const sanitizedCustomFields = sanitizeCustomFields(source.customFields)
|
||||
const customFields = sanitizedCustomFields.map((field) => { ... })
|
||||
// NEW
|
||||
import { normalizeDefinitions } from '~/shared/utils/customFields'
|
||||
const customFields = normalizeDefinitions(source.customFields)
|
||||
import { mergeDefinitionsWithValues } from '~/shared/utils/customFields'
|
||||
const customFields = mergeDefinitionsWithValues(source.customFields, [])
|
||||
```
|
||||
|
||||
Note: The category editor needs additional properties (`optionsText` for textarea display). These can be computed in the editor component itself rather than in the structure normalization.
|
||||
**`optionsText` is now included** in `CustomFieldInput` (added in the type definition). `mergeDefinitionsWithValues` already computes `optionsText` from `options.join('\n')`, so all category editor textareas (`v-model="field.optionsText"`) will work without changes.
|
||||
|
||||
**Important:** `normalizeStructureForEditor` is also used by `useMachineDetailCustomFields.ts` (imported as `normalizeStructureForEditor` from `~/shared/modelUtils`). Make sure the change doesn't break the machine detail page — it should be fine since the new output includes the same `options` array plus the `optionsText` string.
|
||||
|
||||
- [ ] **Step 3: Run lint + typecheck**
|
||||
|
||||
@@ -900,7 +932,7 @@ git commit -m "refactor(custom-fields) : clean category editor structure files"
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Delete old files + final cleanup
|
||||
## Task 9: Delete old files + final cleanup
|
||||
|
||||
**Files:**
|
||||
- Delete: `frontend/app/shared/utils/entityCustomFieldLogic.ts`
|
||||
|
||||
Reference in New Issue
Block a user