Add new dropdown search
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
:piece-types="availablePieceTypes"
|
||||
:lock-type="lockRootType"
|
||||
:locked-type-label="displayedRootTypeLabel"
|
||||
:allow-subcomponents="allowSubcomponents"
|
||||
is-root
|
||||
/>
|
||||
</div>
|
||||
@@ -43,6 +44,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowSubcomponents: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
@@ -55,6 +60,7 @@ const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||
|
||||
const availablePieceTypes = computed(() => pieceTypes.value ?? [])
|
||||
const availableComponentTypes = computed(() => componentTypes.value ?? [])
|
||||
const allowSubcomponents = computed(() => props.allowSubcomponents !== false)
|
||||
|
||||
const fallbackRootTypeLabel = computed(() => {
|
||||
if (!props.rootTypeId) {
|
||||
@@ -156,4 +162,14 @@ onMounted(async () => {
|
||||
}
|
||||
syncRootType()
|
||||
})
|
||||
|
||||
watch(
|
||||
allowSubcomponents,
|
||||
(allowed) => {
|
||||
if (!allowed && Array.isArray(localStructure.subcomponents) && localStructure.subcomponents.length) {
|
||||
localStructure.subcomponents = []
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -14,21 +14,17 @@
|
||||
<label class="label">
|
||||
<span class="label-text text-xs">Sélectionner un composant</span>
|
||||
</label>
|
||||
<select
|
||||
v-model="assignment.selectedComponentId"
|
||||
class="select select-bordered select-sm"
|
||||
>
|
||||
<option value="">
|
||||
{{ componentOptions.length ? 'Choisir un composant compatible' : 'Aucun composant disponible' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="component in componentOptions"
|
||||
:key="component.id"
|
||||
:value="component.id"
|
||||
>
|
||||
{{ formatComponentOption(component) }}
|
||||
</option>
|
||||
</select>
|
||||
<SearchSelect
|
||||
:model-value="assignment.selectedComponentId || ''"
|
||||
:options="componentOptions"
|
||||
:loading="componentsLoading"
|
||||
size="sm"
|
||||
placeholder="Rechercher un composant..."
|
||||
:empty-text="componentOptions.length ? 'Aucun résultat' : 'Aucun composant disponible'"
|
||||
:option-label="componentOptionLabel"
|
||||
:option-description="componentOptionDescription"
|
||||
@update:modelValue="(value) => { assignment.selectedComponentId = normalizeSelectionValue(value); }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -56,21 +52,17 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<select
|
||||
v-model="pieceAssignment.selectedPieceId"
|
||||
class="select select-bordered select-xs"
|
||||
>
|
||||
<option value="">
|
||||
{{ getPieceOptions(pieceAssignment.definition).length ? 'Choisir une pièce' : 'Sélection impossible' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="piece in getPieceOptions(pieceAssignment.definition)"
|
||||
:key="piece.id"
|
||||
:value="piece.id"
|
||||
>
|
||||
{{ formatPieceOption(piece) }}
|
||||
</option>
|
||||
</select>
|
||||
<SearchSelect
|
||||
:model-value="pieceAssignment.selectedPieceId || ''"
|
||||
:options="getPieceOptions(pieceAssignment.definition)"
|
||||
:loading="piecesLoading"
|
||||
size="xs"
|
||||
placeholder="Rechercher une pièce..."
|
||||
:empty-text="getPieceOptions(pieceAssignment.definition).length ? 'Aucun résultat' : 'Aucune pièce disponible'"
|
||||
:option-label="pieceOptionLabel"
|
||||
:option-description="pieceOptionDescription"
|
||||
@update:modelValue="(value) => { pieceAssignment.selectedPieceId = normalizeSelectionValue(value); }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -90,6 +82,8 @@
|
||||
:assignment="subAssignment"
|
||||
:pieces="pieces"
|
||||
:components="components"
|
||||
:components-loading="componentsLoading"
|
||||
:pieces-loading="piecesLoading"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</section>
|
||||
@@ -98,6 +92,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import SearchSelect from '~/components/common/SearchSelect.vue';
|
||||
import type {
|
||||
ComponentModelPiece,
|
||||
ComponentModelStructureNode,
|
||||
@@ -147,11 +142,15 @@ const props = withDefaults(
|
||||
pieces: PieceOption[] | null;
|
||||
components: ComponentOption[] | null;
|
||||
depth?: number;
|
||||
componentsLoading?: boolean;
|
||||
piecesLoading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
depth: 0,
|
||||
pieces: () => [],
|
||||
components: () => [],
|
||||
componentsLoading: false,
|
||||
piecesLoading: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -188,6 +187,29 @@ const componentOptions = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const componentOptionLabel = (component?: ComponentOption | null) => {
|
||||
if (!component) {
|
||||
return 'Composant sans nom';
|
||||
}
|
||||
return component.name || 'Composant sans nom';
|
||||
};
|
||||
|
||||
const componentOptionDescription = (component?: ComponentOption | null) => {
|
||||
if (!component) {
|
||||
return '';
|
||||
}
|
||||
const parts: string[] = [];
|
||||
const typeLabel =
|
||||
component.typeComposant?.name || component.typeComposant?.code || null;
|
||||
if (typeLabel) {
|
||||
parts.push(typeLabel);
|
||||
}
|
||||
if (component.reference) {
|
||||
parts.push(`Ref. ${component.reference}`);
|
||||
}
|
||||
return parts.join(' • ');
|
||||
};
|
||||
|
||||
watch(
|
||||
componentOptions,
|
||||
(options) => {
|
||||
@@ -204,18 +226,6 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const formatComponentOption = (component: ComponentOption) => {
|
||||
const name = component.name || 'Composant sans nom';
|
||||
const reference = component.reference ? ` • Ref. ${component.reference}` : '';
|
||||
const typeLabel =
|
||||
component.typeComposant?.name || component.typeComposant?.code || '';
|
||||
const typed =
|
||||
typeLabel && component.typeComposant?.name !== name
|
||||
? ` (${typeLabel})`
|
||||
: '';
|
||||
return `${name}${typed}${reference}`;
|
||||
};
|
||||
|
||||
const describePieceRequirement = (definition: ComponentModelPiece) => {
|
||||
const parts: string[] = [];
|
||||
if (definition.role) {
|
||||
@@ -288,10 +298,40 @@ const getPieceOptions = (definition: ComponentModelPiece) => {
|
||||
});
|
||||
};
|
||||
|
||||
const formatPieceOption = (piece: PieceOption) => {
|
||||
const name = piece.name || 'Pièce';
|
||||
const reference = piece.reference ? ` • Ref. ${piece.reference}` : '';
|
||||
return `${name}${reference}`;
|
||||
const pieceOptionLabel = (piece?: PieceOption | null) => {
|
||||
if (!piece) {
|
||||
return 'Pièce';
|
||||
}
|
||||
return piece.name || 'Pièce';
|
||||
};
|
||||
|
||||
const pieceOptionDescription = (piece?: PieceOption | null) => {
|
||||
if (!piece) {
|
||||
return '';
|
||||
}
|
||||
const parts: string[] = [];
|
||||
const typeLabel =
|
||||
piece.typePiece?.name || piece.typePiece?.code || null;
|
||||
if (typeLabel) {
|
||||
parts.push(typeLabel);
|
||||
}
|
||||
if (piece.reference) {
|
||||
parts.push(`Ref. ${piece.reference}`);
|
||||
}
|
||||
return parts.join(' • ');
|
||||
};
|
||||
|
||||
const normalizeSelectionValue = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
watch(
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<section v-if="allowSubcomponents" class="space-y-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h4 :class="headingClass">Sous-composants</h4>
|
||||
<button type="button" class="btn btn-outline btn-xs" @click="addSubComponent">
|
||||
@@ -197,6 +197,7 @@
|
||||
:depth="depth + 1"
|
||||
:component-types="componentTypes"
|
||||
:piece-types="pieceTypes"
|
||||
:allow-subcomponents="allowSubcomponents"
|
||||
@remove="removeSubComponent(index)"
|
||||
/>
|
||||
</div>
|
||||
@@ -233,6 +234,7 @@ const props = withDefaults(defineProps<{
|
||||
isRoot?: boolean
|
||||
lockType?: boolean
|
||||
lockedTypeLabel?: string
|
||||
allowSubcomponents?: boolean
|
||||
}>(), {
|
||||
depth: 0,
|
||||
componentTypes: () => [],
|
||||
@@ -240,12 +242,14 @@ const props = withDefaults(defineProps<{
|
||||
isRoot: false,
|
||||
lockType: false,
|
||||
lockedTypeLabel: '',
|
||||
allowSubcomponents: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['remove'])
|
||||
|
||||
const componentTypes = computed(() => props.componentTypes ?? [])
|
||||
const pieceTypes = computed(() => props.pieceTypes ?? [])
|
||||
const allowSubcomponents = computed(() => props.allowSubcomponents !== false)
|
||||
|
||||
const depthClasses = ['', 'ml-4', 'ml-8', 'ml-12', 'ml-16', 'ml-20']
|
||||
const containerClass = computed(() => {
|
||||
@@ -454,6 +458,9 @@ const removePiece = (index: number) => {
|
||||
}
|
||||
|
||||
const addSubComponent = () => {
|
||||
if (!allowSubcomponents.value) {
|
||||
return
|
||||
}
|
||||
ensureArray('subcomponents')
|
||||
props.node.subcomponents.push({
|
||||
typeComposantId: '',
|
||||
@@ -470,6 +477,16 @@ const removeSubComponent = (index: number) => {
|
||||
props.node.subcomponents.splice(index, 1)
|
||||
}
|
||||
|
||||
watch(
|
||||
allowSubcomponents,
|
||||
(allowed) => {
|
||||
if (!allowed && Array.isArray(props.node.subcomponents) && props.node.subcomponents.length) {
|
||||
props.node.subcomponents.splice(0, props.node.subcomponents.length)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(componentTypes, () => {
|
||||
syncComponentType(props.node)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:default-requirement="createDefaultRequirement"
|
||||
:required-fallback="true"
|
||||
:min-fallback="1"
|
||||
:type-loading="loadingComponentTypes"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -51,7 +52,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||
const { componentTypes, loadComponentTypes, loadingComponentTypes } = useComponentTypes()
|
||||
|
||||
const requirements = computed({
|
||||
get: () => props.modelValue,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:default-requirement="createDefaultRequirement"
|
||||
:required-fallback="false"
|
||||
:min-fallback="0"
|
||||
:type-loading="loadingPieceTypes"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -51,7 +52,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||
const { pieceTypes, loadPieceTypes, loadingPieceTypes } = usePieceTypes()
|
||||
|
||||
const requirements = computed({
|
||||
get: () => props.modelValue,
|
||||
|
||||
@@ -29,21 +29,17 @@
|
||||
<span class="label-text">{{ labels.typeSelectLabel }}</span>
|
||||
<span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
:value="requirement[typeField] ?? ''"
|
||||
class="select select-bordered select-sm"
|
||||
required
|
||||
@change="updateRequirement(index, { [typeField]: normalizeTypeValue($event.target.value) })"
|
||||
>
|
||||
<option value="">{{ labels.typePlaceholder }}</option>
|
||||
<option
|
||||
v-for="type in typeOptions"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
<SearchSelect
|
||||
:model-value="normalizeTypeModel(requirement[typeField])"
|
||||
:options="typeOptions"
|
||||
:loading="typeLoading"
|
||||
size="sm"
|
||||
:placeholder="labels.typePlaceholder"
|
||||
:empty-text="typeOptions.length ? 'Aucun résultat' : 'Aucune option disponible'"
|
||||
:option-label="optionLabel"
|
||||
:option-description="optionDescription"
|
||||
@update:modelValue="(value) => updateRequirement(index, { [typeField]: normalizeTypeValue(value) })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
@@ -128,10 +124,12 @@ import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucideTrash2 from '~icons/lucide/trash-2'
|
||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||
|
||||
type Option = {
|
||||
id: string | number
|
||||
name: string
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
type Requirement = Record<string, unknown> & {
|
||||
@@ -193,6 +191,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
typeLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
@@ -202,6 +204,23 @@ const requirements = computed({
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const optionLabel = (option: Option) => {
|
||||
if (!option) {
|
||||
return ''
|
||||
}
|
||||
return option.name || ''
|
||||
}
|
||||
|
||||
const optionDescription = (option: Option) => {
|
||||
if (!option) {
|
||||
return ''
|
||||
}
|
||||
if (typeof option.description === 'string' && option.description.trim()) {
|
||||
return option.description.trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const addRequirement = () => {
|
||||
requirements.value = [
|
||||
...requirements.value,
|
||||
@@ -232,8 +251,18 @@ const parseOptionalNumber = (value: string) => {
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
const normalizeTypeValue = (value: string) => {
|
||||
if (!value) {
|
||||
const normalizeTypeModel = (value: unknown) => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const normalizeTypeValue = (value: string | number | null | undefined) => {
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
return null
|
||||
}
|
||||
return value
|
||||
|
||||
@@ -96,7 +96,10 @@
|
||||
Aperçu :
|
||||
<span class="font-medium text-base-content">{{ componentStructurePreview }}</span>
|
||||
</p>
|
||||
<ComponentModelStructureEditor v-model="componentStructure" />
|
||||
<ComponentModelStructureEditor
|
||||
v-model="componentStructure"
|
||||
:allow-subcomponents="allowComponentSubcomponents"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -147,11 +150,13 @@ const props = withDefaults(defineProps<{
|
||||
saving?: boolean
|
||||
lockCategory?: boolean
|
||||
structureLoading?: boolean
|
||||
allowComponentSubcomponents?: boolean
|
||||
}>(), {
|
||||
initialData: null,
|
||||
saving: false,
|
||||
lockCategory: false,
|
||||
structureLoading: false,
|
||||
allowComponentSubcomponents: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -162,6 +167,7 @@ const emit = defineEmits<{
|
||||
const lockCategory = computed(() => props.lockCategory ?? false)
|
||||
const structureLoading = computed(() => props.structureLoading ?? false)
|
||||
const saving = computed(() => props.saving ?? false)
|
||||
const allowComponentSubcomponents = computed(() => props.allowComponentSubcomponents !== false)
|
||||
|
||||
const form = reactive<ModelTypePayload>({
|
||||
name: '',
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
initial-category="COMPONENT"
|
||||
:initial-data="initialData"
|
||||
:lock-category="true"
|
||||
:allow-component-subcomponents="false"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
mode="create"
|
||||
initial-category="COMPONENT"
|
||||
:lock-category="true"
|
||||
:allow-component-subcomponents="false"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
|
||||
@@ -19,21 +19,17 @@
|
||||
<label class="label">
|
||||
<span class="label-text">Catégorie de composant</span>
|
||||
</label>
|
||||
<select
|
||||
<SearchSelect
|
||||
v-model="selectedTypeId"
|
||||
class="select select-bordered select-sm md:select-md"
|
||||
:options="componentTypeList"
|
||||
:loading="loadingTypes"
|
||||
size="sm"
|
||||
placeholder="Rechercher une catégorie..."
|
||||
empty-text="Aucune catégorie disponible"
|
||||
:option-label="typeOptionLabel"
|
||||
:option-description="typeOptionDescription"
|
||||
:disabled="loadingTypes || submitting"
|
||||
required
|
||||
>
|
||||
<option value="">Sélectionner une catégorie</option>
|
||||
<option
|
||||
v-for="type in componentTypeList"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
/>
|
||||
<p v-if="loadingTypes" class="text-xs text-gray-500 mt-1">
|
||||
Chargement des catégories…
|
||||
</p>
|
||||
@@ -189,6 +185,8 @@
|
||||
:assignment="structureAssignments"
|
||||
:pieces="availablePieces"
|
||||
:components="availableComponents"
|
||||
:pieces-loading="piecesLoading"
|
||||
:components-loading="componentsLoading"
|
||||
/>
|
||||
<p v-else class="text-xs text-error">
|
||||
Impossible de générer les emplacements définis par le squelette.
|
||||
@@ -297,6 +295,7 @@ import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||
import ComponentStructureAssignmentNode, {
|
||||
type StructureAssignmentNode,
|
||||
} from '~/components/ComponentStructureAssignmentNode.vue'
|
||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||
import { useComponentTypes } from '~/composables/useComponentTypes'
|
||||
import { useComposants } from '~/composables/useComposants'
|
||||
import { usePieces } from '~/composables/usePieces'
|
||||
@@ -381,6 +380,12 @@ const componentTypeList = computed<ComponentCatalogType[]>(() =>
|
||||
.filter((item: any) => item?.category === 'COMPONENT') as ComponentCatalogType[],
|
||||
)
|
||||
|
||||
const typeOptionLabel = (type?: ComponentCatalogType) =>
|
||||
type?.name || 'Catégorie'
|
||||
|
||||
const typeOptionDescription = (type?: ComponentCatalogType) =>
|
||||
type?.description ? String(type.description) : ''
|
||||
|
||||
const selectedType = computed(() => {
|
||||
if (!selectedTypeId.value) {
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user