405 lines
13 KiB
Markdown
405 lines
13 KiB
Markdown
# 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"
|
|
```
|