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
13 changes: 13 additions & 0 deletions .changeset/autonumber-textual-ref-fallback-4219.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@object-ui/plugin-detail': patch
---

Inline edit no longer offers a record picker for a spec-spelled `autonumber` field that carries a `reference_to`

`TEXTUAL_REF_FALLBACK_TYPES` — the detail page's one definition of "machine-computed" — spelled the auto-number type `auto_number` only. `@objectstack/spec`, the designer and the metadata importer all spell it `autonumber`, and the set is matched by RAW spelling, so it carried half the type.

The reader that had no gate in front of it is `InlineFieldInput`'s reference fallback, `!!field.reference_to && !TEXTUAL_REF_FALLBACK_TYPES.has(type)`, on exported public API. A field typed `autonumber` keeps a `reference_to` for relational metadata — which is the entire reason this set exists — so it took the lookup branch and rendered the RECORD PICKER: a searchable list of records offered as replacements for a machine-generated identity. The `auto_number` spelling of the identical field rendered the textual fallback, as intended. Both spellings are now members, matching how `plugin-form` carries both in each of its non-input sets.

The editability half of the same report (objectui#4219) was already closed from another direction by #4228, whose shared exclusion resolves aliases before matching — a field typed `autonumber` offers no inline affordance in either host. The two gates are a union, so this fix also removes the union's dependence on which spelling the metadata happens to use: previously `autonumber` was held by the exclusion gate alone and `auto_number` by both, and losing either gate would have re-opened a different half of the defect depending on how the field was authored.

Pins land with it: the reference fallback for `autonumber` (red before this change — the picker really did render), `auto_number` and a real `lookup` as controls in both directions, and set membership asserted directly so the union statement is checked rather than described.
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* The REFERENCE-FALLBACK half of objectui#4219 — `TEXTUAL_REF_FALLBACK_TYPES`
* must carry both spellings of the auto-number type.
*
* #4219 was filed against the editability gate: the set spells the type
* `auto_number` while `@objectstack/spec` (and the designer, and the importer)
* spell it `autonumber`, so a machine-generated identity was hand-editable.
* #4228 closed that half from a different direction — both hosts now also
* consult the fields package's alias-aware exclusion, which resolves
* `autonumber` onto the excluded `auto_number`, so no affordance is offered
* (pinned in `inlineEditTypeCoverage.test.tsx`).
*
* What #4228 does NOT reach is this file's reference fallback:
*
* ```
* const isLookupRef = … || (!!field.reference_to && !TEXTUAL_REF_FALLBACK_TYPES.has(editType));
* ```
*
* That read is `InlineFieldInput`'s own, on EXPORTED public API, with no host
* gate in front of it — a caller rendering the component directly (the SDUI
* surfaces do) gets whatever the set says. So an `autonumber` carrying a
* `reference_to` — computed fields keep one for relational metadata, which is
* the entire reason this set exists — was hijacked into the RECORD PICKER,
* offering the user a list of records to overwrite a machine-generated value
* with. The `auto_number` spelling of the identical field was not.
*
* The two gates are a UNION (`isComputedFieldType`, #3355), and until this fix
* the union's survivability depended on which spelling the metadata happened to
* use: `autonumber` was carried by the exclusion gate alone, `auto_number` by
* both. Losing either gate would then have re-opened a different half of the
* defect depending on the authoring spelling — so the last case here asserts
* membership directly, on the set itself.
*/

import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import {
InlineFieldInput,
INLINE_PLAIN_TEXT_INPUT_TESTID,
TEXTUAL_REF_FALLBACK_TYPES,
} from '../InlineFieldInput';
import { isComputedFieldType } from '../fieldEnrichment';

/** The terminal raw text input — the documented "textual fallback". */
const plainInput = () => screen.queryByTestId(INLINE_PLAIN_TEXT_INPUT_TESTID);

/**
* The record picker's trigger (`LookupField`). Queried by its own test id
* rather than by "some input exists": the terminal text input renders an
* `input` too, so a role/tag heuristic reads the picker and the fallback as the
* same thing — exactly the confusion this pin has to resolve.
*/
const recordPicker = () => screen.queryByTestId(/^lookup-trigger/);

const dataSource = { find: vi.fn(async () => ({ data: [], total: 0 })), findOne: vi.fn() };

const renderField = (type: string, extra: Record<string, unknown> = {}) =>
render(
<InlineFieldInput
field={{ name: 'invoice_no', type, reference_to: 'crm_account', ...extra }}
value="INV-000317"
onChange={vi.fn()}
dataSource={dataSource}
/>,
);

describe('#4219 — the reference fallback carries both auto-number spellings', () => {
it('`autonumber` + `reference_to` renders the textual fallback, NOT the record picker', () => {
renderField('autonumber');
// Before the fix this was the record picker: the set had no `autonumber`,
// so `!!reference_to && !TEXTUAL_REF_FALLBACK_TYPES.has('autonumber')` was
// true and `LookupField` took over the machine-generated identity.
expect(recordPicker()).toBeNull();
expect(plainInput()).not.toBeNull();
expect(plainInput()).toHaveValue('INV-000317');
});

it('`auto_number` + `reference_to` is unchanged (control — the spelling that already worked)', () => {
renderField('auto_number');
expect(recordPicker()).toBeNull();
expect(plainInput()).toHaveValue('INV-000317');
});

it('a real `lookup` still gets the record picker (control — the fix must not over-block)', () => {
renderField('lookup', { name: 'account' });
// The fallback set is about types that merely CARRY a reference; widening
// it must never swallow a type whose value really is a record reference.
expect(recordPicker()).not.toBeNull();
expect(plainInput()).toBeNull();
});

it('both spellings are computed for the gate union, and members of the one set', () => {
// Membership on the set itself, not only through the helper: the union's
// survivability must not depend on which spelling the metadata uses.
expect(TEXTUAL_REF_FALLBACK_TYPES.has('autonumber')).toBe(true);
expect(TEXTUAL_REF_FALLBACK_TYPES.has('auto_number')).toBe(true);
// …and the union reads it from either side (view entry / object schema).
expect(isComputedFieldType('autonumber', undefined)).toBe(true);
expect(isComputedFieldType(undefined, 'autonumber')).toBe(true);
});
});
17 changes: 17 additions & 0 deletions packages/plugin-detail/src/fieldEnrichment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,28 @@ import { isInlineExcludedFieldType } from '@object-ui/fields';
* machine-computed is part of resolving a detail field's shape out of the view
* entry plus the object schema, not the private business of any one component.
* Re-exported from `InlineFieldInput` under its historical name.
*
* **Both spellings of the auto-number type are members** (objectui#4219).
* `@objectstack/spec` — and the designer, and the importer — spell it
* `autonumber`; `auto_number` is the widget map's key, and the alias table
* (`fields/src/field-type-alias.ts`) maps both onto `field:auto_number`. This
* set is matched by RAW spelling, so carrying one of them is carrying half the
* type: `plugin-form` lists both in each of its non-input sets, and this is the
* shape that was missing here. The gap was not academic — the reader that has
* no host gate in front of it is `InlineFieldInput`'s reference fallback
* (`!!field.reference_to && !TEXTUAL_REF_FALLBACK_TYPES.has(type)`), on
* exported public API, so a spec-spelled auto-number carrying a `reference_to`
* resolved into the RECORD PICKER — a list of records offered as replacements
* for a machine-generated identity. The editability half of the same report is
* held by the alias-aware shared exclusion since #4228; the two gates are a
* union, and matching spellings is what stops that union's survivability from
* depending on which spelling the metadata happens to use.
*/
export const TEXTUAL_REF_FALLBACK_TYPES = new Set([
'formula',
'summary',
'rollup',
'autonumber',
'auto_number',
]);

Expand Down
Loading