Pedido do usuário: "faz a posição dos nós persistir entre recargas" — o editor visual da PHASE 60 recalculava o layout do zero a cada carga da página, perdendo qualquer arrasto manual assim que a tela recarregava. `IvrMenu.entryPositionX/Y` + `IvrMenuOption.positionX/Y` (nullable, puramente de apresentação — nunca entram no dialplan compilado, só no layout do canvas). Salvos no banco, nunca localStorage: mesma convenção do resto do app, estado compartilhado entre quem quer que edite o tenant, não por navegador/dispositivo. "Salvar alterações" agora lê a posição de verdade do estado de nós do @xyflow/react (reflete arrastos feitos na sessão), não do estado de conteúdo das opções — os dois tinham ficado dessincronizados desde a PHASE 60. Sem posição salva ainda (menu novo, opção recém-adicionada), continua caindo num layout automático em coluna. Testado ponta a ponta: PATCH com coordenadas específicas (incluindo o nó "Entrada"), GET de volta confirma os mesmos valores, e a página carregada de novo com uma sessão real (cookie de login, não só a API crua) já embute essas coordenadas nos props iniciais do componente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
88 lines
2.2 KiB
TypeScript
88 lines
2.2 KiB
TypeScript
import { Type } from "class-transformer";
|
|
import { ArrayMaxSize, ArrayMinSize, IsArray, IsIn, IsNumber, IsOptional, IsString, Matches, MaxLength, ValidateNested } from "class-validator";
|
|
import { ALLOWED_IVR_DIGITS } from "@b2bcall/telephony";
|
|
import { IsSafeDialplanData } from "../../dialplan/dto/safe-dialplan-data.validator";
|
|
|
|
export class IvrMenuOptionDto {
|
|
@IsIn(ALLOWED_IVR_DIGITS)
|
|
digit!: string;
|
|
|
|
@IsString()
|
|
@Matches(/^[a-zA-Z0-9_-]{1,40}$/, { message: "destinationNumber deve ser alfanumérico (1 a 40 caracteres)" })
|
|
destinationNumber!: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(80)
|
|
destinationContext?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(80)
|
|
label?: string;
|
|
|
|
// Posição do nó no editor visual (PHASE 61) — puramente de
|
|
// apresentação, nunca afeta o dialplan compilado.
|
|
@IsOptional()
|
|
@IsNumber()
|
|
positionX?: number;
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
positionY?: number;
|
|
}
|
|
|
|
export class CreateIvrMenuDto {
|
|
@IsString()
|
|
@MaxLength(80)
|
|
name!: string;
|
|
|
|
// Vira o `context` do dialplan compilado — derivado do nome no
|
|
// frontend (slug), mas validado aqui como qualquer outro context.
|
|
@IsString()
|
|
@Matches(/^[a-z0-9-]{1,60}$/, { message: "context deve ser minúsculo, com letras/números/hífen (1 a 60 caracteres)" })
|
|
context!: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(500)
|
|
@IsSafeDialplanData({ message: "greeting usa uma função não permitida (ver docs/EXTENSIONS.md — nunca system/bg_system/curl/db/lua/shell)" })
|
|
greeting?: string;
|
|
|
|
@IsArray()
|
|
@ArrayMinSize(1)
|
|
@ArrayMaxSize(12)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => IvrMenuOptionDto)
|
|
options!: IvrMenuOptionDto[];
|
|
}
|
|
|
|
export class UpdateIvrMenuDto {
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(80)
|
|
name?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(500)
|
|
@IsSafeDialplanData({ message: "greeting usa uma função não permitida (ver docs/EXTENSIONS.md — nunca system/bg_system/curl/db/lua/shell)" })
|
|
greeting?: string;
|
|
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMinSize(1)
|
|
@ArrayMaxSize(12)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => IvrMenuOptionDto)
|
|
options?: IvrMenuOptionDto[];
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
entryPositionX?: number;
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
entryPositionY?: number;
|
|
}
|