Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/action-param-inline-lookup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@object-ui/app-shell": patch
"@object-ui/i18n": patch
---

fix(app-shell): give inline `lookup` action params a real record picker (#3405)

An action parameter declared inline as `{ name: 'inspector', type: 'lookup',
reference: 'sys_user' }` always rendered as a plain text input asking the user
to paste a record id (UUID) — a supervisor assigning an inspector had to go
find that person's UUID by hand, while the same reference field picks records
by name in the create/edit dialog.

`paramToField()` degrades a picker param to text when it has no `referenceTo`
target, and `referenceTo` was only ever populated on the field-backed branch of
`resolveActionParams()`. The inline branch dropped the authored `reference`
key entirely (as did the spec schema, which stripped it as unknown), so an
inline picker could never reach `<LookupField>` no matter how it was authored.

- `resolveActionParam()` now maps an inline `reference` onto `referenceTo` — on
the inline branch, on the missing-field fallback branch, and as an override
on the field-backed branch (matching how every other inline value overrides
the resolved field).
- The text degradation now warns in dev naming the offending param, since with
`@objectstack/spec` rejecting a targetless inline picker at parse time it
means the metadata is broken, not merely partial.
- The fallback's placeholder and help text no longer claim "a picker is coming
soon" — the picker has shipped, and the message now says the parameter has no
reference object configured. Updated across all 10 locales.
21 changes: 20 additions & 1 deletion packages/app-shell/src/utils/paramToField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* `FORM_FIELD_TYPES` + this drift test makes that class of bug impossible to
* reintroduce silently.
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { FORM_FIELD_TYPES } from '@object-ui/fields';
import type { ActionParamDef } from '@object-ui/core';
import { paramToField, resolveParamWidgetType } from './paramToField';
Expand Down Expand Up @@ -130,6 +130,25 @@ describe('paramToField', () => {
expect(paramToField(p({ type: 'reference' }))).toMatchObject({ type: 'text' });
});

// #3405 — the fallback is now a broken-metadata signal, not a normal path,
// so it must be audible in dev instead of silently handing the user a box
// that wants a raw UUID.
it('warns in dev when a picker param degrades for want of a target', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
paramToField(p({ name: 'inspector', type: 'lookup' }));
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toContain('inspector');
expect(warn.mock.calls[0][0]).toContain('reference');

warn.mockClear();
paramToField(p({ name: 'inspector', type: 'lookup', referenceTo: 'sys_user' }));
expect(warn).not.toHaveBeenCalled();
} finally {
warn.mockRestore();
}
});

it('user params keep their picker without needing referenceTo (implicit sys_user)', () => {
expect(paramToField(p({ type: 'user' }))).toMatchObject({ type: 'user' });
});
Expand Down
13 changes: 13 additions & 0 deletions packages/app-shell/src/utils/paramToField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,23 @@ const LOOKUP_WIDGET_TYPES = new Set(['lookup', 'master_detail']);
* `referenceTo` target renders as a plain text input (the picker cannot query
* without a target object) — preserving the dialog's long-standing behavior
* for partially-resolved metadata.
*
* That fallback is now a last resort, not an expected path (#3405): inline
* params declare `reference` and field-backed ones inherit it, and the spec
* rejects a targetless inline picker at parse time. Reaching it means the
* metadata is broken or partial, so say so in dev instead of silently handing
* the user a box that wants a raw UUID.
*/
export function paramToField(param: ActionParamDef): Record<string, any> {
let type = resolveParamWidgetType(param.type);
if (LOOKUP_WIDGET_TYPES.has(type) && !param.referenceTo) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[ActionParamDialog] Param "${param.name}" is type "${param.type}" but has no reference target, ` +
'so it degrades to a plain record-id text input. Declare `reference: \'<object>\'` on the param, ' +
'or make it field-backed (`{ field: \'<lookup_field>\' }`) to inherit the target.',
);
}
type = 'text';
}

Expand Down
63 changes: 63 additions & 0 deletions packages/app-shell/src/utils/resolveActionParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,66 @@ describe('resolveActionParams — widget config (ADR-0059)', () => {
});
});
});

/**
* #3405 — an inline `lookup` param carries its own picker target.
*
* Real-world break (PLAT-DEF-005): a QC dispatch action declared
* `{ name: 'inspector', type: 'lookup', reference: 'sys_user' }`. The key was
* unknown to both the spec schema and this resolver, so it was dropped twice
* over and `paramToField()` degraded the param to a "paste the record id
* (UUID)" text box — a supervisor had to go find a user's UUID by hand.
*/
describe('resolveActionParams — inline lookup reference target (#3405)', () => {
const lookupCtx = () =>
ctx({
objects: [
{
name: 'quality_dispatch',
fields: {
inspector: { type: 'lookup', label: '质检人', reference: 'sys_user' },
reviewer: { type: 'lookup', label: 'Reviewer', reference_to: 'sys_user', display_field: 'name' },
},
},
],
objectName: 'quality_dispatch',
});

it('maps an inline `reference` onto referenceTo so the picker can query', () => {
const params: RawActionParam[] = [
{ name: 'inspector', label: '质检员', type: 'lookup', reference: 'sys_user', required: true },
];
expect(resolveActionParams(params, lookupCtx())[0]).toMatchObject({
name: 'inspector',
type: 'lookup',
referenceTo: 'sys_user',
});
});

it('still inherits the target from the referenced field when field-backed', () => {
expect(resolveActionParams([{ field: 'inspector' }], lookupCtx())[0]).toMatchObject({
type: 'lookup',
referenceTo: 'sys_user',
});
expect(resolveActionParams([{ field: 'reviewer' }], lookupCtx())[0]).toMatchObject({
referenceTo: 'sys_user',
displayField: 'name',
});
});

it('lets an inline reference override the field metadata', () => {
const params: RawActionParam[] = [{ field: 'inspector', reference: 'sys_member' }];
expect(resolveActionParams(params, lookupCtx())[0].referenceTo).toBe('sys_member');
});

it('keeps the inline reference on the missing-field fallback branch', () => {
const params: RawActionParam[] = [
{ field: 'does_not_exist', type: 'lookup', reference: 'sys_user' },
];
expect(resolveActionParams(params, lookupCtx())[0].referenceTo).toBe('sys_user');
});

it('leaves referenceTo undefined for a non-picker inline param', () => {
expect(resolveActionParams([{ name: 'note', type: 'textarea' }], lookupCtx())[0].referenceTo).toBeUndefined();
});
});
18 changes: 17 additions & 1 deletion packages/app-shell/src/utils/resolveActionParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export interface RawActionParam {
accept?: string[];
/** Max upload size in bytes for `file`/`image` params. */
maxSize?: number;
/**
* Reference target for an INLINE `lookup`/`master_detail` param (#3405) —
* the object whose records the picker searches. Field-backed params inherit
* it from the referenced field instead (see `lookupExtras` below). Spelled
* `reference` to match `FieldSchema.reference` / `ActionParamSchema.reference`.
*/
reference?: string;
/**
* Visibility predicate (CEL) — mirrors the spec `ActionParamSchema.visible`.
* The server serialises it through `ExpressionInputSchema` as an
Expand Down Expand Up @@ -172,6 +179,10 @@ export function resolveActionParam(
multiple: param.multiple,
accept: param.accept,
maxSize: param.maxSize,
// Inline picker target (#3405). Without this an inline `lookup` param
// could never reach `<LookupField>` — `paramToField()` degrades a
// targetless picker to a raw record-id text input.
referenceTo: param.reference,
};
}

Expand All @@ -196,6 +207,9 @@ export function resolveActionParam(
multiple: param.multiple,
accept: param.accept,
maxSize: param.maxSize,
// The field is unresolvable, so an inline `reference` is the only picker
// target available — keep it rather than dropping to a text input.
referenceTo: param.reference,
};
}

Expand All @@ -211,7 +225,9 @@ export function resolveActionParam(
const isLookupResolvedType = resolvedType === 'lookup' || resolvedType === 'reference';
const lookupExtras: Partial<ActionParamDef> = isLookupResolvedType
? {
referenceTo: field.reference_to ?? field.reference,
// Inline `reference` wins, matching how every other inline value
// overrides the resolved field (#3405).
referenceTo: param.reference ?? field.reference_to ?? field.reference,
displayField: field.display_field ?? field.reference_field,
idField: field.id_field,
descriptionField: field.description_field,
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,8 +1496,8 @@ const ar = {
uploading: "جارٍ الرفع…",
defaultActionTitle: "إجراء",
ok: "موافق",
lookupPlaceholder: "لصق معرف السجل (UUID) لـ {{label}}",
lookupHelpText: "أدخل معرف سجل الكائن المرجع. سيتم إضافة أداة اختيار قريباً.",
lookupPlaceholder: "معرف السجل لـ {{label}}",
lookupHelpText: "لم يتم تكوين كائن مرجعي لهذه المعلمة، لذا فإن أداة اختيار السجلات غير متاحة. أدخل معرف السجل، أو اطلب من المسؤول تصحيح معلمة الإجراء.",
},
actionConfirm: {
title: "تأكيد الإجراء",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1498,8 +1498,8 @@ const de = {
uploading: "Wird hochgeladen…",
defaultActionTitle: "Aktion",
ok: "OK",
lookupPlaceholder: "Datensatz-ID (UUID) für {{label}} einfügen",
lookupHelpText: "Geben Sie die Datensatz-ID des referenzierten Objekts ein. Eine Auswahlhilfe kommt bald.",
lookupPlaceholder: "Datensatz-ID für {{label}}",
lookupHelpText: "Für diesen Parameter ist kein Referenzobjekt konfiguriert, daher ist die Datensatzauswahl nicht verfügbar. Geben Sie eine Datensatz-ID ein oder bitten Sie einen Administrator, den Aktionsparameter zu korrigieren.",
},
actionConfirm: {
title: "Aktion bestätigen",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2135,8 +2135,8 @@ const en = {
description: 'Please provide the required information to continue.',
selectPlaceholder: 'Select {{label}}',
requiredError: '{{label}} is required',
lookupPlaceholder: 'Paste the {{label}} record id (UUID)',
lookupHelpText: 'Enter the record id of the referenced object. A picker is coming soon.',
lookupPlaceholder: 'Record id for {{label}}',
lookupHelpText: 'No reference object is configured for this parameter, so the record picker is unavailable. Enter a record id, or ask an administrator to fix the action parameter.',
cancel: 'Cancel',
confirm: 'Confirm',
uploading: 'Uploading…',
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1498,8 +1498,8 @@ const es = {
uploading: "Subiendo…",
defaultActionTitle: "Acción",
ok: "Aceptar",
lookupPlaceholder: "Pegar ID de registro (UUID) para {{label}}",
lookupHelpText: "Ingrese el ID del registro del objeto referenciado. Pronto se añadirá un selector.",
lookupPlaceholder: "ID de registro para {{label}}",
lookupHelpText: "Este parámetro no tiene un objeto de referencia configurado, por lo que el selector de registros no está disponible. Ingrese un ID de registro o pida a un administrador que corrija el parámetro de la acción.",
},
actionConfirm: {
title: "Confirmar acción",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,8 +1496,8 @@ const fr = {
uploading: "Téléversement…",
defaultActionTitle: "Action",
ok: "OK",
lookupPlaceholder: "Coller l'ID d'enregistrement (UUID) pour {{label}}",
lookupHelpText: "Entrez l'ID de l'enregistrement de l'objet référencé. Un sélecteur sera bientôt ajouté.",
lookupPlaceholder: "ID d'enregistrement pour {{label}}",
lookupHelpText: "Aucun objet de référence n'est configuré pour ce paramètre, le sélecteur d'enregistrement est donc indisponible. Saisissez un ID d'enregistrement ou demandez à un administrateur de corriger le paramètre d'action.",
},
actionConfirm: {
title: "Confirmer l’action",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1498,8 +1498,8 @@ const ja = {
uploading: "アップロード中…",
defaultActionTitle: "アクション",
ok: "OK",
lookupPlaceholder: "{{label}} のレコードID(UUID)を貼り付け",
lookupHelpText: "参照オブジェクトのレコードIDを入力してください。ピッカーは近日公開予定です。",
lookupPlaceholder: "{{label}} のレコードID",
lookupHelpText: "このパラメータには参照オブジェクトが設定されていないため、レコードピッカーを利用できません。レコードIDを直接入力するか、管理者にアクションパラメータの修正を依頼してください。",
},
actionConfirm: {
title: "操作の確認",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1498,8 +1498,8 @@ const ko = {
uploading: "업로드 중…",
defaultActionTitle: "작업",
ok: "확인",
lookupPlaceholder: "{{label}}의 레코드 ID(UUID) 붙여넣기",
lookupHelpText: "참조된 개체의 레코드 ID를 입력하세요. 선택기가 곧 추가됩니다.",
lookupPlaceholder: "{{label}}의 레코드 ID",
lookupHelpText: "이 매개변수에 참조 개체가 설정되어 있지 않아 레코드 선택기를 사용할 수 없습니다. 레코드 ID를 직접 입력하거나 관리자에게 작업 매개변수 수정을 요청하세요.",
},
actionConfirm: {
title: "작업 확인",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,8 +1496,8 @@ const pt = {
uploading: "Enviando…",
defaultActionTitle: "Ação",
ok: "OK",
lookupPlaceholder: "Colar ID do registro (UUID) para {{label}}",
lookupHelpText: "Insira o ID do registro do objeto referenciado. Um seletor será adicionado em breve.",
lookupPlaceholder: "ID do registro para {{label}}",
lookupHelpText: "Este parâmetro não tem um objeto de referência configurado, portanto o seletor de registros não está disponível. Insira um ID de registro ou peça a um administrador para corrigir o parâmetro da ação.",
},
actionConfirm: {
title: "Confirmar ação",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,8 +1496,8 @@ const ru = {
uploading: "Загрузка…",
defaultActionTitle: "Действие",
ok: "ОК",
lookupPlaceholder: "Вставить ID записи (UUID) для {{label}}",
lookupHelpText: "Введите ID записи связанного объекта. Выбор записи будет добавлен позже.",
lookupPlaceholder: "ID записи для {{label}}",
lookupHelpText: "Для этого параметра не настроен объект ссылки, поэтому выбор записи недоступен. Введите ID записи или попросите администратора исправить параметр действия.",
},
actionConfirm: {
title: "Подтвердите действие",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2215,8 +2215,8 @@ const zh = {
description: '请填写以下信息以继续操作。',
selectPlaceholder: '请选择 {{label}}',
requiredError: '{{label}} 为必填项',
lookupPlaceholder: '粘贴 {{label}} 的记录 ID(UUID)',
lookupHelpText: '请输入引用记录的 ID。可视化选择器即将上线。',
lookupPlaceholder: '{{label}} 的记录 ID',
lookupHelpText: '该参数未配置引用对象,无法使用记录选择器。请直接填写记录 ID,或联系管理员修正该动作参数。',
cancel: '取消',
confirm: '确认',
uploading: '上传中…',
Expand Down
Loading