From 9f522a6dbbe82d80d32da28103f0bb5ac2528326 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 28 Oct 2025 18:08:08 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20g=C3=A9rer=20l'ordre=20des=20champs=20p?= =?UTF-8?q?ersonnalis=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 18 ++++++++ prisma/schema.prisma | 1 + src/common/constants/component-includes.ts | 5 ++- src/common/constants/custom-field.constant.ts | 1 + src/common/mappers/model-type.mapper.spec.ts | 5 ++- src/common/mappers/model-type.mapper.ts | 22 +++++++--- .../mappers/type-machine.mapper.spec.ts | 4 ++ src/common/mappers/type-machine.mapper.ts | 11 +++-- .../model-types.repository.spec.ts | 2 + .../type-machines.repository.spec.ts | 1 + src/custom-fields/custom-fields.service.ts | 12 +++++- src/machines/machines.service.ts | 41 +++++++++++++++---- src/pieces/pieces.service.spec.ts | 1 + src/pieces/pieces.service.ts | 29 +++++++++---- src/shared/dto/type.dto.ts | 8 ++++ 15 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 prisma/migrations/20251107140000_custom_field_order/migration.sql diff --git a/prisma/migrations/20251107140000_custom_field_order/migration.sql b/prisma/migrations/20251107140000_custom_field_order/migration.sql new file mode 100644 index 0000000..b621e66 --- /dev/null +++ b/prisma/migrations/20251107140000_custom_field_order/migration.sql @@ -0,0 +1,18 @@ +-- Introduce an order index for custom fields so their ordering can be persisted. + +ALTER TABLE "custom_fields" +ADD COLUMN "orderIndex" INTEGER NOT NULL DEFAULT 0; + +WITH ranked AS ( + SELECT + "id", + ROW_NUMBER() OVER ( + PARTITION BY "typeMachineId", "typeComposantId", "typePieceId" + ORDER BY "createdAt", "id" + ) - 1 AS rn + FROM "custom_fields" +) +UPDATE "custom_fields" +SET "orderIndex" = ranked.rn +FROM ranked +WHERE ranked."id" = "custom_fields"."id"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d4ff08d..24e9801 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -245,6 +245,7 @@ model CustomField { required Boolean @default(false) defaultValue String? options String[] // Pour les champs de type SELECT + orderIndex Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/common/constants/component-includes.ts b/src/common/constants/component-includes.ts index c5efef1..77d5149 100644 --- a/src/common/constants/component-includes.ts +++ b/src/common/constants/component-includes.ts @@ -6,12 +6,15 @@ const CUSTOM_FIELD_SELECT = { type: true, required: true, options: true, + orderIndex: true, } as const; export const COMPONENT_WITH_RELATIONS_INCLUDE = { typeComposant: { include: { - customFields: true, + customFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, constructeurs: true, diff --git a/src/common/constants/custom-field.constant.ts b/src/common/constants/custom-field.constant.ts index 65bd78d..19cbdd2 100644 --- a/src/common/constants/custom-field.constant.ts +++ b/src/common/constants/custom-field.constant.ts @@ -4,4 +4,5 @@ export const CUSTOM_FIELD_SELECT = { type: true, required: true, options: true, + orderIndex: true, } as const; diff --git a/src/common/mappers/model-type.mapper.spec.ts b/src/common/mappers/model-type.mapper.spec.ts index 5c1ba45..389a2a4 100644 --- a/src/common/mappers/model-type.mapper.spec.ts +++ b/src/common/mappers/model-type.mapper.spec.ts @@ -43,7 +43,10 @@ describe('ModelTypeMapper', () => { description: 'Desc', notes: 'Desc', }); - expect(input.customFields?.create?.[0]).toMatchObject({ name: 'Field' }); + expect(input.customFields?.create?.[0]).toMatchObject({ + name: 'Field', + orderIndex: 0, + }); expect((input as any).componentSkeleton).toEqual({ pieces: [ { diff --git a/src/common/mappers/model-type.mapper.ts b/src/common/mappers/model-type.mapper.ts index ca64975..471342c 100644 --- a/src/common/mappers/model-type.mapper.ts +++ b/src/common/mappers/model-type.mapper.ts @@ -12,12 +12,18 @@ import type { import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant'; export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = { - customFields: { select: CUSTOM_FIELD_SELECT }, + customFields: { + select: CUSTOM_FIELD_SELECT, + orderBy: { orderIndex: 'asc' }, + }, composants: true, }; export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = { - pieceCustomFields: { select: CUSTOM_FIELD_SELECT }, + pieceCustomFields: { + select: CUSTOM_FIELD_SELECT, + orderBy: { orderIndex: 'asc' }, + }, pieceRequirements: true, pieces: true, }; @@ -42,11 +48,12 @@ export class ModelTypeMapper { notes: description ?? null, customFields: customFields ? { - create: customFields.map((field) => ({ + create: customFields.map((field, index) => ({ name: field.name, type: field.type, required: field.required ?? false, options: field.options, + orderIndex: field.orderIndex ?? index, })), } : undefined, @@ -97,11 +104,12 @@ export class ModelTypeMapper { notes: description ?? null, pieceCustomFields: customFields ? { - create: customFields.map((field) => ({ + create: customFields.map((field, index) => ({ name: field.name, type: field.type, required: field.required ?? false, options: field.options, + orderIndex: field.orderIndex ?? index, })), } : undefined, @@ -165,11 +173,12 @@ export class ModelTypeMapper { return []; } - return fields.map((field) => ({ + return fields.map((field, index) => ({ name: field.name, type: field.type, required: field.required ?? false, options: field.options, + orderIndex: field.orderIndex ?? index, })); } @@ -180,11 +189,12 @@ export class ModelTypeMapper { return []; } - return fields.map((field) => ({ + return fields.map((field, index) => ({ name: field.name, type: field.type, required: field.required ?? false, options: field.options, + orderIndex: field.orderIndex ?? index, })); } } diff --git a/src/common/mappers/type-machine.mapper.spec.ts b/src/common/mappers/type-machine.mapper.spec.ts index 3fa3791..013919a 100644 --- a/src/common/mappers/type-machine.mapper.spec.ts +++ b/src/common/mappers/type-machine.mapper.spec.ts @@ -33,6 +33,9 @@ describe('TypeMachineMapper', () => { const input = TypeMachineMapper.toCreateInput(baseDto as any); expect(input.customFields?.create).toHaveLength(1); + expect(input.customFields?.create?.[0]).toMatchObject({ + orderIndex: 0, + }); expect(input.componentRequirements?.create?.[0]).toMatchObject({ label: 'Comp', minCount: 2, @@ -61,6 +64,7 @@ describe('TypeMachineMapper', () => { type: 'string', required: true, options: ['a'], + orderIndex: 0, }, ]); }); diff --git a/src/common/mappers/type-machine.mapper.ts b/src/common/mappers/type-machine.mapper.ts index 8e2bae0..3e9a662 100644 --- a/src/common/mappers/type-machine.mapper.ts +++ b/src/common/mappers/type-machine.mapper.ts @@ -17,7 +17,10 @@ type RequirementDto = { }; export const TYPE_MACHINE_DEFAULT_INCLUDE: Prisma.TypeMachineInclude = { - customFields: { select: CUSTOM_FIELD_SELECT }, + customFields: { + select: CUSTOM_FIELD_SELECT, + orderBy: { orderIndex: 'asc' }, + }, componentRequirements: { include: { typeComposant: true }, orderBy: { orderIndex: 'asc' }, @@ -81,11 +84,12 @@ export class TypeMachineMapper { } return { - create: fields.map((field) => ({ + create: fields.map((field, index) => ({ name: field.name, type: field.type, required: field.required ?? false, options: field.options, + orderIndex: field.orderIndex ?? index, })), }; } @@ -95,11 +99,12 @@ export class TypeMachineMapper { return []; } - return fields.map((field) => ({ + return fields.map((field, index) => ({ name: field.name, type: field.type, required: field.required ?? false, options: field.options, + orderIndex: field.orderIndex ?? index, })); } diff --git a/src/common/repositories/model-types.repository.spec.ts b/src/common/repositories/model-types.repository.spec.ts index cf87aa4..f227645 100644 --- a/src/common/repositories/model-types.repository.spec.ts +++ b/src/common/repositories/model-types.repository.spec.ts @@ -45,6 +45,7 @@ describe('ModelTypesRepository', () => { type: 'string', required: true, options: [], + orderIndex: 0, typeComposantId: 'comp-id', }, ], @@ -63,6 +64,7 @@ describe('ModelTypesRepository', () => { type: 'string', required: false, options: [], + orderIndex: 0, typePieceId: 'piece-id', }, ], diff --git a/src/common/repositories/type-machines.repository.spec.ts b/src/common/repositories/type-machines.repository.spec.ts index 41039ac..8f01c5e 100644 --- a/src/common/repositories/type-machines.repository.spec.ts +++ b/src/common/repositories/type-machines.repository.spec.ts @@ -44,6 +44,7 @@ describe('TypeMachinesRepository', () => { type: 'string', required: true, options: [], + orderIndex: 0, typeMachineId: 'machine-id', }, ], diff --git a/src/custom-fields/custom-fields.service.ts b/src/custom-fields/custom-fields.service.ts index 9fc15ab..ccac487 100644 --- a/src/custom-fields/custom-fields.service.ts +++ b/src/custom-fields/custom-fields.service.ts @@ -209,12 +209,20 @@ export class CustomFieldsService { if (existingField) { targetCustomFieldId = existingField.id; } else { + const normalizedType = (customFieldType || 'text').trim() || 'text'; + const normalizedRequired = !!customFieldRequired; + const orderScope = { [customFieldTypeField]: typeId } as const; + const nextOrderIndex = await this.prisma.customField.count({ + where: orderScope, + }); + const createdField = await this.prisma.customField.create({ data: { name: normalizedName, - type: (customFieldType || 'text').trim() || 'text', - required: !!customFieldRequired, + type: normalizedType, + required: normalizedRequired, options: normalizedOptions, + orderIndex: nextOrderIndex, [customFieldTypeField]: typeId, }, }); diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index c577da1..5044bc1 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -17,15 +17,21 @@ const CUSTOM_FIELD_SELECT = { type: true, required: true, options: true, + orderIndex: true, } as const; const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = { - customFields: { select: CUSTOM_FIELD_SELECT }, + customFields: { + select: CUSTOM_FIELD_SELECT, + orderBy: { orderIndex: 'asc' }, + }, componentRequirements: { include: { typeComposant: { include: { - customFields: true, + customFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, }, @@ -34,7 +40,9 @@ const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = { include: { typePiece: { include: { - customFields: true, + customFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, }, @@ -52,7 +60,9 @@ const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = { constructeurs: true, typePiece: { include: { - customFields: true, + customFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, documents: true, @@ -62,7 +72,9 @@ const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = { include: { typePiece: { include: { - customFields: true, + customFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, }, @@ -78,7 +90,9 @@ const buildComponentLinkInclude = ( constructeurs: true, typeComposant: { include: { - customFields: true, + customFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, customFieldValues: { @@ -93,7 +107,9 @@ const buildComponentLinkInclude = ( include: { typeComposant: { include: { - customFields: true, + customFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, }, @@ -1663,12 +1679,23 @@ export class MachinesService { let targetCustomFieldId = existingCustomFieldId; if (!targetCustomFieldId) { + const whereClause = typeMachineId + ? { typeMachineId } + : { typeMachineId: null }; + const nextOrderIndex = + typeof customField.orderIndex === 'number' + ? customField.orderIndex + : await prisma.customField.count({ + where: whereClause, + }); + const createdCustomField = await prisma.customField.create({ data: { name: customField.name, type: customField.type, required: customField.required || false, options: customField.options || [], + orderIndex: nextOrderIndex, typeMachineId: typeMachineId ?? null, }, }); diff --git a/src/pieces/pieces.service.spec.ts b/src/pieces/pieces.service.spec.ts index 0b1a4dc..563ab29 100644 --- a/src/pieces/pieces.service.spec.ts +++ b/src/pieces/pieces.service.spec.ts @@ -19,6 +19,7 @@ describe('PiecesService', () => { customField: { findMany: jest.fn(), create: jest.fn(), + update: jest.fn(), }, customFieldValue: { findMany: jest.fn(), diff --git a/src/pieces/pieces.service.ts b/src/pieces/pieces.service.ts index bcf68f5..02e120d 100644 --- a/src/pieces/pieces.service.ts +++ b/src/pieces/pieces.service.ts @@ -8,7 +8,9 @@ import type { PieceModelStructure } from '../shared/types/inventory'; const PIECE_WITH_RELATIONS_INCLUDE = { typePiece: { include: { - pieceCustomFields: true, + pieceCustomFields: { + orderBy: { orderIndex: 'asc' }, + }, }, }, constructeurs: true, @@ -286,23 +288,35 @@ export class PiecesService { const existing = await this.prisma.customField.findMany({ where: { typePieceId }, - select: { id: true, name: true }, + select: { id: true, name: true, orderIndex: true }, }); const existingByName = new Map( existing.map((field) => [ this.normalizeIdentifier(field.name) ?? field.name, - field.id, + field, ]), ); - for (const field of customFields) { + for (let index = 0; index < customFields.length; index += 1) { + const field = customFields[index]; if (!field) { continue; } const name = this.normalizeIdentifier(field.name); - if (!name || existingByName.has(name)) { + if (!name) { + continue; + } + + const existingField = existingByName.get(name); + if (existingField) { + if (existingField.orderIndex !== index) { + await this.prisma.customField.update({ + where: { id: existingField.id }, + data: { orderIndex: index }, + }); + } continue; } @@ -316,12 +330,13 @@ export class PiecesService { type, required, options, + orderIndex: index, typePieceId, }, - select: { id: true }, + select: { id: true, name: true, orderIndex: true }, }); - existingByName.set(name, created.id); + existingByName.set(name, created); } } diff --git a/src/shared/dto/type.dto.ts b/src/shared/dto/type.dto.ts index 97895b0..5380e58 100644 --- a/src/shared/dto/type.dto.ts +++ b/src/shared/dto/type.dto.ts @@ -36,6 +36,10 @@ export class CreateCustomFieldDto { @IsOptional() @IsArray() options?: string[]; // Pour les champs de type SELECT + + @IsOptional() + @IsInt() + orderIndex?: number; } export class UpdateCustomFieldDto { @@ -54,6 +58,10 @@ export class UpdateCustomFieldDto { @IsOptional() @IsArray() options?: string[]; + + @IsOptional() + @IsInt() + orderIndex?: number; } export class TypeMachineComponentRequirementDto {