Replace ~2900 lines across 9 files with ~400 lines in 2 files: - shared/utils/customFields.ts (types + pure helpers) - composables/useCustomFieldInputs.ts (reactive composable) Migrated all consumers: - Backend: add defaultValue to API Platform serialization groups - Standalone pages: component edit/create, piece edit/create, product edit/create/detail - Machine page: MachineCustomFieldsCard, MachineInfoCard, useMachineDetailCustomFields - Hierarchy: ComponentItem, PieceItem - Shared: CustomFieldDisplay, CustomFieldInputGrid - Category editor: componentStructure.ts Deleted: - entityCustomFieldLogic.ts (335 lines) - customFieldUtils.ts (440 lines) - customFieldFormUtils.ts (404 lines) - useEntityCustomFields.ts (181 lines) - customFieldFormUtils.test.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
189 lines
5.5 KiB
Vue
189 lines
5.5 KiB
Vue
<template>
|
|
<div
|
|
v-if="fields.length"
|
|
:class="containerClass"
|
|
>
|
|
<h5 v-if="showHeader" class="text-sm font-medium text-base-content/80 mb-3">
|
|
{{ title }}
|
|
</h5>
|
|
<div :class="layoutClass">
|
|
<div
|
|
v-for="(field, index) in fields"
|
|
:key="fieldKey(field, index)"
|
|
class="form-control"
|
|
>
|
|
<label class="label">
|
|
<span class="label-text text-sm">{{
|
|
field.name
|
|
}}</span>
|
|
<span
|
|
v-if="field.required"
|
|
class="label-text-alt text-error"
|
|
>*</span>
|
|
</label>
|
|
|
|
<!-- Mode édition -->
|
|
<template v-if="isFieldEditable(field)">
|
|
<!-- Champ de type TEXT -->
|
|
<input
|
|
v-if="field.type === 'text'"
|
|
:value="field.value ?? ''"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:required="field.required"
|
|
@input="onInput(field, ($event.target as HTMLInputElement).value)"
|
|
@blur="onBlur(field)"
|
|
>
|
|
|
|
<!-- Champ de type NUMBER -->
|
|
<input
|
|
v-else-if="field.type === 'number'"
|
|
:value="field.value ?? ''"
|
|
type="number"
|
|
class="input input-bordered input-sm"
|
|
:required="field.required"
|
|
@input="onInput(field, ($event.target as HTMLInputElement).value)"
|
|
@blur="onBlur(field)"
|
|
>
|
|
|
|
<!-- Champ de type SELECT -->
|
|
<select
|
|
v-else-if="field.type === 'select'"
|
|
:value="field.value ?? ''"
|
|
class="select select-bordered select-sm"
|
|
:required="field.required"
|
|
@change="onInput(field, ($event.target as HTMLSelectElement).value)"
|
|
@blur="onBlur(field)"
|
|
>
|
|
<option value="">
|
|
Sélectionner...
|
|
</option>
|
|
<option
|
|
v-for="option in field.options"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
|
|
<!-- Champ de type BOOLEAN -->
|
|
<div
|
|
v-else-if="field.type === 'boolean'"
|
|
class="flex items-center gap-2"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox checkbox-sm"
|
|
:checked="String(field.value).toLowerCase() === 'true'"
|
|
@change="onBooleanChange(field, ($event.target as HTMLInputElement).checked)"
|
|
>
|
|
<span class="text-sm">{{
|
|
String(field.value).toLowerCase() === "true" ? "Oui" : "Non"
|
|
}}</span>
|
|
</div>
|
|
|
|
<!-- Champ de type DATE -->
|
|
<input
|
|
v-else-if="field.type === 'date'"
|
|
:value="field.value ?? ''"
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
:required="field.required"
|
|
@input="onInput(field, ($event.target as HTMLInputElement).value)"
|
|
@blur="onBlur(field)"
|
|
>
|
|
|
|
<!-- Champ de type TEXTAREA -->
|
|
<textarea
|
|
v-else-if="field.type === 'textarea'"
|
|
:value="field.value ?? ''"
|
|
class="textarea textarea-bordered textarea-sm"
|
|
:required="field.required"
|
|
@input="onInput(field, ($event.target as HTMLTextAreaElement).value)"
|
|
@blur="onBlur(field)"
|
|
/>
|
|
|
|
<!-- Fallback: input text -->
|
|
<input
|
|
v-else
|
|
:value="field.value ?? ''"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:required="field.required"
|
|
@input="onInput(field, ($event.target as HTMLInputElement).value)"
|
|
@blur="onBlur(field)"
|
|
>
|
|
</template>
|
|
|
|
<!-- Mode lecture seule -->
|
|
<template v-else>
|
|
<div class="input input-bordered input-sm bg-base-200">
|
|
{{ formatValueForDisplay(field) }}
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { fieldKey, formatValueForDisplay, type CustomFieldInput } from '~/shared/utils/customFields'
|
|
|
|
const props = defineProps<{
|
|
fields: CustomFieldInput[]
|
|
isEditMode: boolean
|
|
columns?: 1 | 2
|
|
title?: string
|
|
showHeader?: boolean
|
|
withTopBorder?: boolean
|
|
editable?: boolean
|
|
emitBlur?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'field-input': [field: CustomFieldInput, value: string]
|
|
'field-blur': [field: CustomFieldInput]
|
|
}>()
|
|
|
|
const layoutClass = computed(() =>
|
|
props.columns === 2
|
|
? 'grid grid-cols-1 md:grid-cols-2 gap-4'
|
|
: 'space-y-3',
|
|
)
|
|
|
|
const title = computed(() => props.title ?? 'Champs personnalisés')
|
|
const showHeader = computed(() => props.showHeader ?? true)
|
|
const containerClass = computed(() =>
|
|
props.withTopBorder === false
|
|
? ''
|
|
: 'mt-4 pt-4 border-t border-base-200',
|
|
)
|
|
const editable = computed(() => props.editable ?? true)
|
|
const emitBlur = computed(() => props.emitBlur ?? true)
|
|
|
|
function isFieldEditable(field: CustomFieldInput) {
|
|
return props.isEditMode && editable.value && !field.readOnly
|
|
}
|
|
|
|
function onInput(field: CustomFieldInput, value: string) {
|
|
field.value = value
|
|
emit('field-input', field, value)
|
|
}
|
|
|
|
function onBooleanChange(field: CustomFieldInput, checked: boolean) {
|
|
const value = checked ? 'true' : 'false'
|
|
field.value = value
|
|
emit('field-input', field, value)
|
|
if (emitBlur.value) {
|
|
emit('field-blur', field)
|
|
}
|
|
}
|
|
|
|
function onBlur(field: CustomFieldInput) {
|
|
if (emitBlur.value) {
|
|
emit('field-blur', field)
|
|
}
|
|
}
|
|
</script>
|