docs(custom-fields) : add spec and implementation plans for machine context custom fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
# Machine Context Custom Fields — Frontend Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> **Parallel plan:** This is the frontend half. The backend plan is at `2026-04-02-machine-context-fields-backend.md`. Both can run in parallel on separate worktrees — they share no files. Frontend tests requiring the API will need the backend done first.
|
||||
|
||||
**Goal:** Add `machineContextOnly` toggle in structure editors, filter context fields from standalone pages, and display/edit them in the machine detail view.
|
||||
|
||||
**Architecture:** Structure editors get a checkbox per field. The machine-detail transform propagates `contextCustomFields`/`contextCustomFieldValues` from the API link response onto the component/piece objects. Standalone entity views filter these out. Machine view displays them in a separate "Champs contextuels" section using the existing `CustomFieldDisplay` component, saving via upsert with the link ID.
|
||||
|
||||
**Tech Stack:** Nuxt 4, Vue 3 Composition API, TypeScript, DaisyUI 5
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-02-machine-context-custom-fields-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### Modify
|
||||
- `frontend/app/shared/types/inventory.ts` — add `machineContextOnly` to custom field types
|
||||
- `frontend/app/components/PieceModelStructureEditor.vue` — add checkbox toggle
|
||||
- `frontend/app/composables/usePieceStructureEditorLogic.ts` — add default in `createEmptyField()`
|
||||
- `frontend/app/components/StructureNodeEditor.vue` — add checkbox toggle
|
||||
- `frontend/app/composables/useStructureNodeCrud.ts` — add default in `addCustomField()`
|
||||
- `frontend/app/composables/useEntityCustomFields.ts` — filter out `machineContextOnly` fields
|
||||
- `frontend/app/composables/useMachineDetailCustomFields.ts` — propagate context fields, filter from normal merge
|
||||
- `frontend/app/components/ComponentItem.vue` — display context custom fields section
|
||||
- `frontend/app/components/PieceItem.vue` — display context custom fields section
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Types — add `machineContextOnly`
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/shared/types/inventory.ts`
|
||||
|
||||
- [ ] **Step 1: Add `machineContextOnly` to `ComponentModelCustomField`**
|
||||
|
||||
In the `ComponentModelCustomField` interface (around line 14), add:
|
||||
|
||||
```typescript
|
||||
machineContextOnly?: boolean
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `machineContextOnly` to `PieceModelCustomField`**
|
||||
|
||||
In the `PieceModelCustomField` interface (around line 65), add:
|
||||
|
||||
```typescript
|
||||
machineContextOnly?: boolean
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd frontend && git add app/shared/types/inventory.ts
|
||||
git commit -m "feat(custom-fields) : add machineContextOnly to custom field types"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Structure editors — add toggle
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/components/PieceModelStructureEditor.vue:122-125`
|
||||
- Modify: `frontend/app/composables/usePieceStructureEditorLogic.ts:283-290`
|
||||
- Modify: `frontend/app/components/StructureNodeEditor.vue:121-125`
|
||||
- Modify: `frontend/app/composables/useStructureNodeCrud.ts:49-62`
|
||||
|
||||
- [ ] **Step 1: Add toggle in `PieceModelStructureEditor.vue`**
|
||||
|
||||
After the "Obligatoire" checkbox block (line 125, after `</div>` closing the required checkbox), add:
|
||||
|
||||
```vue
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<input v-model="field.machineContextOnly" type="checkbox" class="checkbox checkbox-xs">
|
||||
Contexte machine uniquement
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `usePieceStructureEditorLogic.ts` — 3 functions**
|
||||
|
||||
**a) `createEmptyField`** (line 283) — add `machineContextOnly: false` to the returned object:
|
||||
|
||||
```typescript
|
||||
const createEmptyField = (orderIndex: number): EditorField => ({
|
||||
uid: createUid('field'),
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
optionsText: '',
|
||||
machineContextOnly: false,
|
||||
orderIndex,
|
||||
})
|
||||
```
|
||||
|
||||
**b) `toEditorField`** (line 78-91) — add `machineContextOnly` to the returned object, after the `orderIndex` line (line 90):
|
||||
|
||||
```typescript
|
||||
machineContextOnly: Boolean(input?.machineContextOnly),
|
||||
```
|
||||
|
||||
**c) `buildPayload`** (line 160-165) — add `machineContextOnly` to the `payload` object after `orderIndex` (line 164):
|
||||
|
||||
```typescript
|
||||
machineContextOnly: Boolean(field.machineContextOnly),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add toggle in `StructureNodeEditor.vue`**
|
||||
|
||||
After the "Obligatoire" checkbox closing `</div>` (line 123) and **before** the `<textarea>` for select options (line 124), add:
|
||||
|
||||
```vue
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<input v-model="field.machineContextOnly" type="checkbox" class="checkbox checkbox-xs">
|
||||
Contexte machine uniquement
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `addCustomField` in `useStructureNodeCrud.ts`**
|
||||
|
||||
In `addCustomField` (line 49), add `machineContextOnly: false` to the pushed object (line 53-60):
|
||||
|
||||
```typescript
|
||||
fields.push({
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
optionsText: '',
|
||||
options: [],
|
||||
machineContextOnly: false,
|
||||
orderIndex: nextIndex,
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run lint + typecheck**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
|
||||
Expected: 0 errors.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd frontend && git add .
|
||||
git commit -m "feat(custom-fields) : add machineContextOnly toggle in structure editors"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Filter context fields on standalone pages + machine-detail transform
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/composables/useEntityCustomFields.ts:42-49`
|
||||
- Modify: `frontend/app/composables/useMachineDetailCustomFields.ts:141-154,241-256`
|
||||
|
||||
- [ ] **Step 1: Filter `machineContextOnly` from `displayedCustomFields` in `useEntityCustomFields.ts`**
|
||||
|
||||
Update the `displayedCustomFields` computed (line 42):
|
||||
|
||||
```typescript
|
||||
const displayedCustomFields = computed(() =>
|
||||
dedupeMergedFields(
|
||||
mergeFieldDefinitionsWithValues(
|
||||
definitionSources.value,
|
||||
entity().customFieldValues,
|
||||
),
|
||||
).filter((field: any) => !field.machineContextOnly && !field.customField?.machineContextOnly),
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Filter `machineContextOnly` from normal customFields in machine-detail transform and propagate context data**
|
||||
|
||||
In `frontend/app/composables/useMachineDetailCustomFields.ts`:
|
||||
|
||||
**For pieces** — In `transformCustomFields` (line 70), the returned object is built at line 141. Replace the `customFields,` line (line 143) with:
|
||||
|
||||
```typescript
|
||||
customFields: customFields.filter((f: AnyRecord) => !f.machineContextOnly && !f.customField?.machineContextOnly),
|
||||
contextCustomFields: piece.contextCustomFields ?? [],
|
||||
contextCustomFieldValues: piece.contextCustomFieldValues ?? [],
|
||||
```
|
||||
|
||||
**For components** — In `transformComponentCustomFields` (line 158), the returned object is built at line 241. Replace the `customFields,` line (line 243) with:
|
||||
|
||||
```typescript
|
||||
customFields: customFields.filter((f: AnyRecord) => !f.machineContextOnly && !f.customField?.machineContextOnly),
|
||||
contextCustomFields: component.contextCustomFields ?? [],
|
||||
contextCustomFieldValues: component.contextCustomFieldValues ?? [],
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run lint + typecheck**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
|
||||
Expected: 0 errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd frontend && git add .
|
||||
git commit -m "feat(custom-fields) : filter machineContextOnly from standalone and machine-detail views"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Display context fields in machine view — `ComponentItem.vue`
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/components/ComponentItem.vue`
|
||||
|
||||
Context fields are on the `component` object (set by the transform in Task 3), not as separate props.
|
||||
|
||||
- [ ] **Step 1: Add template section**
|
||||
|
||||
After the existing `CustomFieldDisplay` block (line 195), add:
|
||||
|
||||
```vue
|
||||
<!-- Context custom fields (machine-specific) -->
|
||||
<div v-if="mergedContextFields.length" class="mt-4">
|
||||
<h4 class="text-xs font-semibold text-base-content/70 mb-2">
|
||||
Champs contextuels
|
||||
</h4>
|
||||
<CustomFieldDisplay
|
||||
:fields="mergedContextFields"
|
||||
:is-edit-mode="isEditMode"
|
||||
:columns="2"
|
||||
@field-blur="updateContextCustomField"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add imports and script logic**
|
||||
|
||||
**IMPORTANT:** `ComponentItem.vue` uses `<script setup>` WITHOUT `lang="ts"`. Do NOT use TypeScript annotations (`: any`, `: string`, etc.) in any code added to this file.
|
||||
|
||||
Add these imports (they are NOT already present in the component):
|
||||
|
||||
```javascript
|
||||
import { mergeFieldDefinitionsWithValues, dedupeMergedFields } from '~/shared/utils/entityCustomFieldLogic'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
```
|
||||
|
||||
Add after the existing `useEntityCustomFields` block (around line 348):
|
||||
|
||||
```javascript
|
||||
const { upsertCustomFieldValue } = useCustomFields()
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const mergedContextFields = computed(() => {
|
||||
const definitions = props.component?.contextCustomFields ?? []
|
||||
const values = props.component?.contextCustomFieldValues ?? []
|
||||
if (!definitions.length && !values.length) return []
|
||||
return dedupeMergedFields(
|
||||
mergeFieldDefinitionsWithValues(definitions, values),
|
||||
)
|
||||
})
|
||||
|
||||
const updateContextCustomField = async (field) => {
|
||||
const linkId = props.component?.linkId
|
||||
if (!linkId || !field) return
|
||||
|
||||
const customFieldId = field.customFieldId || field.customField?.id
|
||||
if (!customFieldId) return
|
||||
|
||||
const result = await upsertCustomFieldValue(
|
||||
customFieldId,
|
||||
'machineComponentLink',
|
||||
linkId,
|
||||
field.value ?? '',
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(`Champ contextuel "${field.name || field.customField?.name}" mis à jour`)
|
||||
} else {
|
||||
showError(`Erreur lors de la mise à jour du champ contextuel`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run lint + typecheck**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd frontend && git add app/components/ComponentItem.vue
|
||||
git commit -m "feat(custom-fields) : display context custom fields in ComponentItem"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Display context fields in machine view — `PieceItem.vue`
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/app/components/PieceItem.vue`
|
||||
|
||||
- [ ] **Step 1: Add template section**
|
||||
|
||||
After the existing `CustomFieldDisplay` block (line 236), add:
|
||||
|
||||
```vue
|
||||
<!-- Context custom fields (machine-specific) -->
|
||||
<div v-if="mergedContextFields.length" class="mt-4">
|
||||
<h4 class="text-xs font-semibold text-base-content/70 mb-2">
|
||||
Champs contextuels
|
||||
</h4>
|
||||
<CustomFieldDisplay
|
||||
:fields="mergedContextFields"
|
||||
:is-edit-mode="isEditMode"
|
||||
:columns="2"
|
||||
@field-blur="updateContextCustomField"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add imports and script logic**
|
||||
|
||||
**IMPORTANT:** `PieceItem.vue` uses `<script setup>` WITHOUT `lang="ts"`. Do NOT use TypeScript annotations in any code added to this file.
|
||||
|
||||
Add these imports (they are NOT already present in the component):
|
||||
|
||||
```javascript
|
||||
import { mergeFieldDefinitionsWithValues, dedupeMergedFields } from '~/shared/utils/entityCustomFieldLogic'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
```
|
||||
|
||||
Add after the existing `useEntityCustomFields` block (around line 366):
|
||||
|
||||
```javascript
|
||||
const { upsertCustomFieldValue } = useCustomFields()
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const mergedContextFields = computed(() => {
|
||||
const definitions = props.piece?.contextCustomFields ?? []
|
||||
const values = props.piece?.contextCustomFieldValues ?? []
|
||||
if (!definitions.length && !values.length) return []
|
||||
return dedupeMergedFields(
|
||||
mergeFieldDefinitionsWithValues(definitions, values),
|
||||
)
|
||||
})
|
||||
|
||||
const updateContextCustomField = async (field) => {
|
||||
const linkId = props.piece?.linkId
|
||||
if (!linkId || !field) return
|
||||
|
||||
const customFieldId = field.customFieldId || field.customField?.id
|
||||
if (!customFieldId) return
|
||||
|
||||
const result = await upsertCustomFieldValue(
|
||||
customFieldId,
|
||||
'machinePieceLink',
|
||||
linkId,
|
||||
field.value ?? '',
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(`Champ contextuel "${field.name || field.customField?.name}" mis à jour`)
|
||||
} else {
|
||||
showError(`Erreur lors de la mise à jour du champ contextuel`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run lint + typecheck**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd frontend && git add app/components/PieceItem.vue
|
||||
git commit -m "feat(custom-fields) : display context custom fields in PieceItem"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Frontend build verification
|
||||
|
||||
- [ ] **Step 1: Run full build**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: Build succeeds with no errors.
|
||||
|
||||
- [ ] **Step 2: Final commit — update submodule pointer (from main repo)**
|
||||
|
||||
```bash
|
||||
cd /home/matthieu/dev_malio/Inventory
|
||||
git add frontend
|
||||
git commit -m "chore : update frontend submodule for context custom fields"
|
||||
```
|
||||
Reference in New Issue
Block a user