picklist corrigido

This commit is contained in:
2026-06-17 11:41:12 -03:00
parent 2cb4b2f975
commit b89012d5c1
8 changed files with 287 additions and 17 deletions
+22 -7
View File
@@ -16,7 +16,12 @@ import {
createClarizenClient,
} from "./clarizen/client.js";
import { formatFieldHint } from "./metadata/field-hints.js";
import { isPickListField } from "./metadata/picklist.js";
import { getEntityMetadata, pickEntityType, pickField } from "./metadata/picker.js";
import {
promptPickListValue,
type FieldValueSelection,
} from "./parse/prompt-picklist.js";
import {
formatValueForDisplay,
parseFieldValue,
@@ -27,7 +32,7 @@ import { fetchAllEntities } from "./query/fetch-all.js";
import { promptEntityFilter } from "./query/prompt-filter.js";
import { buildRunLog, writeRunLog } from "./log/run-log.js";
import { bulkUpdateField } from "./update/bulk-field.js";
import type { FieldDescription, JsonValue } from "@arco/clarizen-lib";
import type { Clarizen, FieldDescription, JsonValue } from "@arco/clarizen-lib";
function exitCancel(): never {
cancel("Operação cancelada.");
@@ -35,8 +40,13 @@ function exitCancel(): never {
}
async function promptFieldValue(
clarizen: Clarizen,
field: FieldDescription,
): Promise<JsonValue | symbol> {
): Promise<FieldValueSelection | symbol> {
if (isPickListField(field)) {
return promptPickListValue(clarizen, field);
}
console.log("\n" + formatFieldHint(field) + "\n");
if (field.nullable) {
@@ -48,7 +58,7 @@ async function promptFieldValue(
],
});
if (isCancel(action)) return action;
if (action === "clear") return null;
if (action === "clear") return { value: null, displayLabel: "(vazio / null)" };
}
while (true) {
@@ -64,7 +74,8 @@ async function promptFieldValue(
if (isCancel(raw)) return raw;
try {
return parseFieldValue(raw, field);
const value = parseFieldValue(raw, field);
return { value };
} catch (err) {
if (err instanceof ValueParseError) {
console.error(`\n Erro: ${err.message}\n`);
@@ -107,8 +118,11 @@ async function main(): Promise<void> {
const { field } = fieldChoice;
const fieldName = field.name!;
const parsedValue = await promptFieldValue(field);
if (isCancel(parsedValue)) exitCancel();
const valueSelection = await promptFieldValue(clarizen, field);
if (isCancel(valueSelection)) exitCancel();
const parsedValue = valueSelection.value;
const valueDisplayLabel = valueSelection.displayLabel;
const fetchSpinner = spinner();
fetchSpinner.start("Buscando objetos...");
@@ -137,7 +151,7 @@ Resumo da operação:
Tipo: ${entityLabel}
Filtro: ${formatFilterSummary(entityFilter)}
Campo: ${field.label ?? fieldName} (${fieldName}) — ${field.type ?? "?"}
Valor: ${formatValueForDisplay(parsedValue)}
Valor: ${formatValueForDisplay(parsedValue, valueDisplayLabel)}
Objetos: ${records.length.toLocaleString("pt-BR")} registro(s)
`);
@@ -168,6 +182,7 @@ Resumo da operação:
field,
fieldName,
parsedValue,
valueDisplayLabel,
records,
result,
}),
+4
View File
@@ -26,6 +26,7 @@ export interface RunLog {
fieldLabel?: string;
fieldType?: string;
value: JsonValue;
valueLabel?: string;
};
summary: {
total: number;
@@ -43,6 +44,7 @@ export interface BuildRunLogParams {
field: FieldDescription;
fieldName: string;
parsedValue: JsonValue;
valueDisplayLabel?: string;
records: EntityRecord[];
result: BulkUpdateResult;
}
@@ -60,6 +62,7 @@ export function buildRunLog(params: BuildRunLogParams): RunLog {
field,
fieldName,
parsedValue,
valueDisplayLabel,
records,
result,
} = params;
@@ -101,6 +104,7 @@ export function buildRunLog(params: BuildRunLogParams): RunLog {
fieldLabel: field.label,
fieldType: field.type,
value: parsedValue,
valueLabel: valueDisplayLabel,
},
summary: {
total: records.length,
+18 -7
View File
@@ -1,4 +1,5 @@
import type { FieldDescription, FieldType } from "@arco/clarizen-lib";
import { isPickListField } from "./picklist.js";
const TYPE_EXAMPLES: Record<FieldType, string> = {
Boolean: "true, false, sim, não",
@@ -20,11 +21,20 @@ export function getFieldTypeExample(type: FieldType | undefined): string {
export function formatFieldHint(field: FieldDescription): string {
const type = field.type ?? "desconhecido";
const example = getFieldTypeExample(field.type);
const lines = [
`Tipo: ${type}`,
`Exemplo: ${example}`,
];
const lines: string[] = [];
if (isPickListField(field)) {
lines.push("Tipo: PickList (lista de opções)");
lines.push("Escolha um valor na lista — não digite o nome exibido manualmente.");
const refType = (field as { referencedEntities?: string[] }).referencedEntities?.[0];
if (refType) {
lines.push(`Opções carregadas de: ${refType}`);
}
} else {
const example = getFieldTypeExample(field.type);
lines.push(`Tipo: ${type}`);
lines.push(`Exemplo: ${example}`);
}
if (field.nullable) {
lines.push("Este campo aceita valor vazio (null) para limpar.");
@@ -39,8 +49,9 @@ export function formatFieldHint(field: FieldDescription): string {
export function formatFieldOptionLabel(field: FieldDescription): string {
const label = field.label?.trim() || field.name || "?";
const name = field.name ?? "?";
const type = field.type ?? "?";
return `${label} (${name}) — ${type}`;
const presentation =
field.presentationType === "PickList" ? "PickList" : (field.type ?? "?");
return `${label} (${name}) — ${presentation}`;
}
export function isUpdatableField(field: FieldDescription): boolean {
+1 -1
View File
@@ -63,7 +63,7 @@ async function pickFromList<T extends { value: string; label: string }>(
return select({ message, options });
}
async function pickFromFiltered<T extends { value: string; label: string }>(
export async function pickFromFiltered<T extends { value: string; label: string }>(
message: string,
allOptions: T[],
filterPrompt: string,
+91
View File
@@ -0,0 +1,91 @@
import type { Clarizen, FieldDescription } from "@arco/clarizen-lib";
import { resolveDisplayField } from "../query/resolve-display-field.js";
const PAGE_SIZE = 500;
export interface FieldWithRefs extends FieldDescription {
referencedEntities?: string[];
}
export interface PickListOption {
id: string;
label: string;
}
export function isPickListField(field: FieldWithRefs): boolean {
return field.presentationType === "PickList";
}
export function getPickListEntityType(field: FieldWithRefs): string | undefined {
return field.referencedEntities?.[0];
}
function extractLabel(
entity: Record<string, unknown>,
displayField: string,
): string | undefined {
const objectFields = entity.objectFields as Record<string, unknown> | undefined;
const value = entity[displayField] ?? objectFields?.[displayField];
if (value === null || value === undefined) return undefined;
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (typeof value === "object" && value !== null && "name" in value) {
const name = (value as { name?: unknown }).name;
if (name !== null && name !== undefined) return String(name);
}
return undefined;
}
function idSuffix(id: string): string {
return id.includes("/") ? id.split("/").pop()! : id;
}
export async function fetchPickListOptions(
clarizen: Clarizen,
pickListTypeName: string,
onProgress?: (count: number) => void,
): Promise<PickListOption[]> {
const meta = await clarizen.metadata.describeMetadata({
typeNames: [pickListTypeName],
flags: ["fields"],
});
const entityMeta = meta.entityDescriptions?.find(
(e) => e.typeName === pickListTypeName,
);
if (!entityMeta) {
throw new Error(`Metadata não encontrada para picklist ${pickListTypeName}.`);
}
const displayField = resolveDisplayField(entityMeta);
const options: PickListOption[] = [];
let from = 0;
while (true) {
const result = await clarizen.data.entityQuery({
typeName: pickListTypeName,
fields: [displayField],
paging: { from, limit: PAGE_SIZE },
});
for (const entity of result.entities) {
if (!entity.id) continue;
const id = String(entity.id);
const label =
extractLabel(entity as Record<string, unknown>, displayField) ??
idSuffix(id);
options.push({ id, label });
}
onProgress?.(options.length);
if (!result.paging?.hasMore) break;
from += PAGE_SIZE;
}
return options.sort((a, b) => a.label.localeCompare(b.label, "pt-BR"));
}
export function formatPickListOptionLabel(option: PickListOption): string {
return `${option.label} (${option.id})`;
}
+134
View File
@@ -0,0 +1,134 @@
import type { Clarizen, JsonValue } from "@arco/clarizen-lib";
import { isCancel, select, spinner, text } from "@clack/prompts";
import { formatFieldHint } from "../metadata/field-hints.js";
import {
fetchPickListOptions,
formatPickListOptionLabel,
getPickListEntityType,
type FieldWithRefs,
} from "../metadata/picklist.js";
import { pickFromFiltered } from "../metadata/picker.js";
import { parseFieldValue, ValueParseError } from "./value.js";
export interface FieldValueSelection {
value: JsonValue;
displayLabel?: string;
}
async function pickPickListOption(
field: FieldWithRefs,
options: Array<{ value: string; label: string }>,
): Promise<string | symbol> {
const fieldLabel = field.label ?? field.name ?? "valor";
return pickFromFiltered(
`Escolha o valor para ${fieldLabel}`,
options,
"Como deseja buscar na lista?",
);
}
async function promptPickListSelection(
clarizen: Clarizen,
field: FieldWithRefs,
pickListTypeName: string,
): Promise<FieldValueSelection | symbol> {
const loadSpinner = spinner();
loadSpinner.start(`Carregando opções de ${pickListTypeName}...`);
const pickListOptions = await fetchPickListOptions(
clarizen,
pickListTypeName,
(count) => {
loadSpinner.message(
`Carregando opções de ${pickListTypeName}... (${count})`,
);
},
);
loadSpinner.stop(`${pickListOptions.length} opção(ões) carregada(s).`);
if (pickListOptions.length === 0) {
console.log(
`\nNenhuma opção encontrada para ${pickListTypeName}. Informe o ID manualmente.\n`,
);
return promptManualEntityValue(field);
}
const selectOptions = pickListOptions.map((o) => ({
value: o.id,
label: formatPickListOptionLabel(o),
}));
const selectedId = await pickPickListOption(field, selectOptions);
if (isCancel(selectedId)) return selectedId;
const match = pickListOptions.find((o) => o.id === selectedId);
return {
value: { id: selectedId },
displayLabel: match
? `${match.label} (${match.id})`
: String(selectedId),
};
}
async function promptManualEntityValue(
field: FieldWithRefs,
): Promise<FieldValueSelection | symbol> {
while (true) {
const raw = await text({
message: "Informe o ID da entidade (ex: /Tipo/guid)",
validate: (value) => (!value?.trim() ? "Valor obrigatório." : undefined),
});
if (isCancel(raw)) return raw;
try {
const value = parseFieldValue(raw, field);
return { value, displayLabel: formatEntityDisplayLabel(value) };
} catch (err) {
if (err instanceof ValueParseError) {
console.error(`\n Erro: ${err.message}\n`);
continue;
}
throw err;
}
}
}
function formatEntityDisplayLabel(value: JsonValue): string | undefined {
if (value && typeof value === "object" && "id" in value) {
const id = (value as { id?: unknown }).id;
if (typeof id === "string") return id;
}
return undefined;
}
export async function promptPickListValue(
clarizen: Clarizen,
field: FieldWithRefs,
): Promise<FieldValueSelection | symbol> {
console.log("\n" + formatFieldHint(field) + "\n");
if (field.nullable) {
const action = await select({
message: "O que deseja fazer com este campo?",
options: [
{ value: "set", label: "Definir um valor" },
{ value: "clear", label: "Limpar campo (null)" },
],
});
if (isCancel(action)) return action;
if (action === "clear") {
return { value: null, displayLabel: "(vazio / null)" };
}
}
const pickListTypeName = getPickListEntityType(field);
if (!pickListTypeName) {
console.log(
"\nPicklist sem tipo de referência na metadata. Informe o ID manualmente.\n",
);
return promptManualEntityValue(field);
}
return promptPickListSelection(clarizen, field, pickListTypeName);
}
+9 -1
View File
@@ -111,8 +111,16 @@ export function parseFieldValue(
return parseByType(raw, field.type);
}
export function formatValueForDisplay(value: JsonValue): string {
export function formatValueForDisplay(
value: JsonValue,
displayLabel?: string,
): string {
if (displayLabel) return displayLabel;
if (value === null) return "(vazio / null)";
if (typeof value === "string") return `"${value}"`;
if (typeof value === "object" && value !== null && "id" in value) {
const id = (value as { id?: unknown }).id;
if (typeof id === "string") return id;
}
return JSON.stringify(value);
}