feat: gérer l'ordre des champs personnalisés
This commit is contained in:
@@ -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";
|
||||||
@@ -245,6 +245,7 @@ model CustomField {
|
|||||||
required Boolean @default(false)
|
required Boolean @default(false)
|
||||||
defaultValue String?
|
defaultValue String?
|
||||||
options String[] // Pour les champs de type SELECT
|
options String[] // Pour les champs de type SELECT
|
||||||
|
orderIndex Int @default(0)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ const CUSTOM_FIELD_SELECT = {
|
|||||||
type: true,
|
type: true,
|
||||||
required: true,
|
required: true,
|
||||||
options: true,
|
options: true,
|
||||||
|
orderIndex: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const COMPONENT_WITH_RELATIONS_INCLUDE = {
|
export const COMPONENT_WITH_RELATIONS_INCLUDE = {
|
||||||
typeComposant: {
|
typeComposant: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
constructeurs: true,
|
constructeurs: true,
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export const CUSTOM_FIELD_SELECT = {
|
|||||||
type: true,
|
type: true,
|
||||||
required: true,
|
required: true,
|
||||||
options: true,
|
options: true,
|
||||||
|
orderIndex: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ describe('ModelTypeMapper', () => {
|
|||||||
description: 'Desc',
|
description: 'Desc',
|
||||||
notes: '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({
|
expect((input as any).componentSkeleton).toEqual({
|
||||||
pieces: [
|
pieces: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ import type {
|
|||||||
import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant';
|
import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant';
|
||||||
|
|
||||||
export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
||||||
customFields: { select: CUSTOM_FIELD_SELECT },
|
customFields: {
|
||||||
|
select: CUSTOM_FIELD_SELECT,
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
composants: true,
|
composants: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
||||||
pieceCustomFields: { select: CUSTOM_FIELD_SELECT },
|
pieceCustomFields: {
|
||||||
|
select: CUSTOM_FIELD_SELECT,
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
pieceRequirements: true,
|
pieceRequirements: true,
|
||||||
pieces: true,
|
pieces: true,
|
||||||
};
|
};
|
||||||
@@ -42,11 +48,12 @@ export class ModelTypeMapper {
|
|||||||
notes: description ?? null,
|
notes: description ?? null,
|
||||||
customFields: customFields
|
customFields: customFields
|
||||||
? {
|
? {
|
||||||
create: customFields.map((field) => ({
|
create: customFields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -97,11 +104,12 @@ export class ModelTypeMapper {
|
|||||||
notes: description ?? null,
|
notes: description ?? null,
|
||||||
pieceCustomFields: customFields
|
pieceCustomFields: customFields
|
||||||
? {
|
? {
|
||||||
create: customFields.map((field) => ({
|
create: customFields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -165,11 +173,12 @@ export class ModelTypeMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map((field) => ({
|
return fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +189,12 @@ export class ModelTypeMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map((field) => ({
|
return fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ describe('TypeMachineMapper', () => {
|
|||||||
const input = TypeMachineMapper.toCreateInput(baseDto as any);
|
const input = TypeMachineMapper.toCreateInput(baseDto as any);
|
||||||
|
|
||||||
expect(input.customFields?.create).toHaveLength(1);
|
expect(input.customFields?.create).toHaveLength(1);
|
||||||
|
expect(input.customFields?.create?.[0]).toMatchObject({
|
||||||
|
orderIndex: 0,
|
||||||
|
});
|
||||||
expect(input.componentRequirements?.create?.[0]).toMatchObject({
|
expect(input.componentRequirements?.create?.[0]).toMatchObject({
|
||||||
label: 'Comp',
|
label: 'Comp',
|
||||||
minCount: 2,
|
minCount: 2,
|
||||||
@@ -61,6 +64,7 @@ describe('TypeMachineMapper', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
options: ['a'],
|
options: ['a'],
|
||||||
|
orderIndex: 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ type RequirementDto = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TYPE_MACHINE_DEFAULT_INCLUDE: Prisma.TypeMachineInclude = {
|
export const TYPE_MACHINE_DEFAULT_INCLUDE: Prisma.TypeMachineInclude = {
|
||||||
customFields: { select: CUSTOM_FIELD_SELECT },
|
customFields: {
|
||||||
|
select: CUSTOM_FIELD_SELECT,
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
componentRequirements: {
|
componentRequirements: {
|
||||||
include: { typeComposant: true },
|
include: { typeComposant: true },
|
||||||
orderBy: { orderIndex: 'asc' },
|
orderBy: { orderIndex: 'asc' },
|
||||||
@@ -81,11 +84,12 @@ export class TypeMachineMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: fields.map((field) => ({
|
create: fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -95,11 +99,12 @@ export class TypeMachineMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map((field) => ({
|
return fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ describe('ModelTypesRepository', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
options: [],
|
options: [],
|
||||||
|
orderIndex: 0,
|
||||||
typeComposantId: 'comp-id',
|
typeComposantId: 'comp-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -63,6 +64,7 @@ describe('ModelTypesRepository', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
options: [],
|
options: [],
|
||||||
|
orderIndex: 0,
|
||||||
typePieceId: 'piece-id',
|
typePieceId: 'piece-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe('TypeMachinesRepository', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
options: [],
|
options: [],
|
||||||
|
orderIndex: 0,
|
||||||
typeMachineId: 'machine-id',
|
typeMachineId: 'machine-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -209,12 +209,20 @@ export class CustomFieldsService {
|
|||||||
if (existingField) {
|
if (existingField) {
|
||||||
targetCustomFieldId = existingField.id;
|
targetCustomFieldId = existingField.id;
|
||||||
} else {
|
} 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({
|
const createdField = await this.prisma.customField.create({
|
||||||
data: {
|
data: {
|
||||||
name: normalizedName,
|
name: normalizedName,
|
||||||
type: (customFieldType || 'text').trim() || 'text',
|
type: normalizedType,
|
||||||
required: !!customFieldRequired,
|
required: normalizedRequired,
|
||||||
options: normalizedOptions,
|
options: normalizedOptions,
|
||||||
|
orderIndex: nextOrderIndex,
|
||||||
[customFieldTypeField]: typeId,
|
[customFieldTypeField]: typeId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,15 +17,21 @@ const CUSTOM_FIELD_SELECT = {
|
|||||||
type: true,
|
type: true,
|
||||||
required: true,
|
required: true,
|
||||||
options: true,
|
options: true,
|
||||||
|
orderIndex: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = {
|
const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = {
|
||||||
customFields: { select: CUSTOM_FIELD_SELECT },
|
customFields: {
|
||||||
|
select: CUSTOM_FIELD_SELECT,
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
componentRequirements: {
|
componentRequirements: {
|
||||||
include: {
|
include: {
|
||||||
typeComposant: {
|
typeComposant: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -34,7 +40,9 @@ const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = {
|
|||||||
include: {
|
include: {
|
||||||
typePiece: {
|
typePiece: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -52,7 +60,9 @@ const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = {
|
|||||||
constructeurs: true,
|
constructeurs: true,
|
||||||
typePiece: {
|
typePiece: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
documents: true,
|
documents: true,
|
||||||
@@ -62,7 +72,9 @@ const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = {
|
|||||||
include: {
|
include: {
|
||||||
typePiece: {
|
typePiece: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -78,7 +90,9 @@ const buildComponentLinkInclude = (
|
|||||||
constructeurs: true,
|
constructeurs: true,
|
||||||
typeComposant: {
|
typeComposant: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
customFieldValues: {
|
customFieldValues: {
|
||||||
@@ -93,7 +107,9 @@ const buildComponentLinkInclude = (
|
|||||||
include: {
|
include: {
|
||||||
typeComposant: {
|
typeComposant: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1663,12 +1679,23 @@ export class MachinesService {
|
|||||||
let targetCustomFieldId = existingCustomFieldId;
|
let targetCustomFieldId = existingCustomFieldId;
|
||||||
|
|
||||||
if (!targetCustomFieldId) {
|
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({
|
const createdCustomField = await prisma.customField.create({
|
||||||
data: {
|
data: {
|
||||||
name: customField.name,
|
name: customField.name,
|
||||||
type: customField.type,
|
type: customField.type,
|
||||||
required: customField.required || false,
|
required: customField.required || false,
|
||||||
options: customField.options || [],
|
options: customField.options || [],
|
||||||
|
orderIndex: nextOrderIndex,
|
||||||
typeMachineId: typeMachineId ?? null,
|
typeMachineId: typeMachineId ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ describe('PiecesService', () => {
|
|||||||
customField: {
|
customField: {
|
||||||
findMany: jest.fn(),
|
findMany: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
},
|
},
|
||||||
customFieldValue: {
|
customFieldValue: {
|
||||||
findMany: jest.fn(),
|
findMany: jest.fn(),
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import type { PieceModelStructure } from '../shared/types/inventory';
|
|||||||
const PIECE_WITH_RELATIONS_INCLUDE = {
|
const PIECE_WITH_RELATIONS_INCLUDE = {
|
||||||
typePiece: {
|
typePiece: {
|
||||||
include: {
|
include: {
|
||||||
pieceCustomFields: true,
|
pieceCustomFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
constructeurs: true,
|
constructeurs: true,
|
||||||
@@ -286,23 +288,35 @@ export class PiecesService {
|
|||||||
|
|
||||||
const existing = await this.prisma.customField.findMany({
|
const existing = await this.prisma.customField.findMany({
|
||||||
where: { typePieceId },
|
where: { typePieceId },
|
||||||
select: { id: true, name: true },
|
select: { id: true, name: true, orderIndex: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingByName = new Map(
|
const existingByName = new Map(
|
||||||
existing.map((field) => [
|
existing.map((field) => [
|
||||||
this.normalizeIdentifier(field.name) ?? field.name,
|
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) {
|
if (!field) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = this.normalizeIdentifier(field.name);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,12 +330,13 @@ export class PiecesService {
|
|||||||
type,
|
type,
|
||||||
required,
|
required,
|
||||||
options,
|
options,
|
||||||
|
orderIndex: index,
|
||||||
typePieceId,
|
typePieceId,
|
||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true, name: true, orderIndex: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
existingByName.set(name, created.id);
|
existingByName.set(name, created);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export class CreateCustomFieldDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
options?: string[]; // Pour les champs de type SELECT
|
options?: string[]; // Pour les champs de type SELECT
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
orderIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateCustomFieldDto {
|
export class UpdateCustomFieldDto {
|
||||||
@@ -54,6 +58,10 @@ export class UpdateCustomFieldDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
options?: string[];
|
options?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
orderIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypeMachineComponentRequirementDto {
|
export class TypeMachineComponentRequirementDto {
|
||||||
|
|||||||
Reference in New Issue
Block a user