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
15 changes: 15 additions & 0 deletions .changeset/11677-approvals-summary-faces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@object-ui/console': patch
---

The approvals inbox's request drawer shows its record summary the way the record page shows the record (objectui#11677).

The summary card now reads the field declarations of the request's object. It asks the same cached metadata read the drawer already makes for `hidden: true`, so no network request is added. With those declarations:

- **Labels.** Each row takes its field's declared label, translated as the record page translates it, instead of the server's snapshot label or a title-cased key.
- **Values.** Each value is drawn by the record page's own cell for its field type. An option shows its label (`Sent`, `EMEA` instead of `sent`, `emea`), a date shows as the date cell shows it, and a currency field shows in its currency. A lookup still shows the record title the server resolved. The lead amount at the top of the card follows the same rule. A `summary` roll-up shows as the record page shows it: the plain stored number, without digit grouping.
- **Rows.** A snapshot key the object does not declare gets no row. Neither does a reference the server could not resolve to a title, because its value is an id.

If the object's metadata cannot be read, the card renders the snapshot as before.

Two rows never share a label. The card now drops the platform's injected ownership column `owner_id` (label "Owner") as bookkeeping, the same way the default list columns do, so an object that also declares its own `owner` field no longer shows two "Owner" rows. If two remaining fields still share a label, each of those rows adds its field key in parentheses. The rows are keyed by field key rather than by label, so React no longer warns about two children with the same key.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* plus the "Flow-initiated" cell, instead of a bare person icon + em dash
* (#2803).
* 3. **Record-card copy and emphasis** — `prettifyKey()` drops a trailing
* `id` token (`owner_id` → "Owner") and `decisionAmountEntry()` promotes
* `id` token (`vendor_id` → "Vendor") and `decisionAmountEntry()` promotes
* the decision amount to a lead figure, excluded from the generic field
* grid so it renders exactly once (#2803).
*
Expand All @@ -41,7 +41,7 @@
* two people to conflate;
* - a **`flow:`-prefixed submitter id** with no `submitter_name` — the shape
* a real flow-initiated request arrives in;
* - a payload carrying **both `owner_id` and `amount`** — `owner_id` proves
* - a payload carrying **both `vendor_id` and `amount`** — `vendor_id` proves
* the id-token trim (a bare `id` key, or one with a resolvable non-`_id`
* name, would not exercise `prettifyKey`'s trim branch at all), `amount`
* proves both the lead-figure promotion and the single-render exclusion.
Expand Down Expand Up @@ -71,9 +71,9 @@ const APP = 'com.objectstack.account';
// all imports) — so the two display-value fixtures live INSIDE it and are
// destructured out below, rather than being plain top-level `const`s the
// hoisted block would close over before they existed.
const { approvalsApiStub, getObjectSchema, ADAPTER, AUTH, I18N, OWNER_DISPLAY, AMOUNT_DISPLAY } = vi.hoisted(() => {
/** The server-resolved display value for `owner_id` — never opaque-dropped. */
const OWNER_DISPLAY = 'Jordan Lee';
const { approvalsApiStub, getObjectSchema, ADAPTER, AUTH, I18N, VENDOR_DISPLAY, AMOUNT_DISPLAY } = vi.hoisted(() => {
/** The server-resolved display value for `vendor_id` — never opaque-dropped. */
const VENDOR_DISPLAY = 'Northwind Labs';
/** The server-formatted decision amount, as `payload_display` would send it. */
const AMOUNT_DISPLAY = 'USD 42,000.00';

Expand All @@ -100,11 +100,14 @@ const { approvalsApiStub, getObjectSchema, ADAPTER, AUTH, I18N, OWNER_DISPLAY, A
// Case 3 fixture: an `_id`-suffixed key (prettifyKey's trim branch) and
// an amount-like key (decisionAmountEntry's lead figure), together.
payload: {
owner_id: 'usr_9f3ma2xk1pqz88a',
// `owner_id` stood here until objectui#11677. It is the platform's
// injected ownership column, which the card now drops as bookkeeping
// (`isSystemManagedField`), so it never reaches `prettifyKey`'s trim.
vendor_id: 'vnd_9f3ma2xk1pqz88a',
amount: 42000,
},
payload_display: {
owner_id: OWNER_DISPLAY,
vendor_id: VENDOR_DISPLAY,
amount: AMOUNT_DISPLAY,
},
};
Expand All @@ -129,7 +132,7 @@ const { approvalsApiStub, getObjectSchema, ADAPTER, AUTH, I18N, OWNER_DISPLAY, A
language: 'en',
};

return { approvalsApiStub, getObjectSchema, ADAPTER, AUTH, I18N, OWNER_DISPLAY, AMOUNT_DISPLAY };
return { approvalsApiStub, getObjectSchema, ADAPTER, AUTH, I18N, VENDOR_DISPLAY, AMOUNT_DISPLAY };
});

vi.mock('@object-ui/i18n', async (importOriginal) => ({
Expand Down Expand Up @@ -228,13 +231,13 @@ describe('Approvals drawer — a flow-initiated request names its origin (object
});

describe('Approvals drawer — record-card copy and emphasis (objectui#2803, #6395)', () => {
it('reads `owner_id` as "Owner" and leads with the decision amount exactly once', async () => {
it('reads `vendor_id` as "Vendor" and leads with the decision amount exactly once', async () => {
const dialog = await openedDrawer();

// prettifyKey: the trailing `id` token is dropped, so the field label
// reads "Owner", not "Owner Id".
expect(within(dialog).getByText('Owner')).toBeInTheDocument();
expect(within(dialog).getByText(OWNER_DISPLAY)).toBeInTheDocument();
// reads "Vendor", not "Vendor Id".
expect(within(dialog).getByText('Vendor')).toBeInTheDocument();
expect(within(dialog).getByText(VENDOR_DISPLAY)).toBeInTheDocument();

// decisionAmountEntry: the amount is promoted to the lead figure AND
// excluded from the generic field grid, so its display value renders
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ async function openedDrawer(): Promise<HTMLElement> {
return dialog;
}

/**
* Every snapshot key declared as a text field. Since objectui#11677 a snapshot
* key the object's metadata does not declare takes no row once that metadata
* has answered, so a fixture that wants a key on the card declares it.
*/
const EVERY_PAYLOAD_FIELD: Record<string, Record<string, unknown>> = Object.fromEntries(
Object.keys(ROW.payload as Record<string, unknown>).map((k) => [k, { type: 'text' }]),
);

/** The card really rendered its business content — run by every denial case. */
function expectSummaryCardIntact(dialog: HTMLElement): void {
for (const value of SIBLING_VALUES) {
Expand All @@ -216,11 +225,7 @@ afterEach(cleanup);
describe('Approvals drawer summary card — `hidden: true` trim (objectui#5565)', () => {
it('drops the hidden field and promotes the seventh into its slot', async () => {
getObjectSchema.mockResolvedValue({
fields: {
subject: { type: 'text' },
diagnosis_code: { type: 'text', hidden: true },
justification: { type: 'text' },
},
fields: { ...EVERY_PAYLOAD_FIELD, diagnosis_code: { type: 'text', hidden: true } },
});
const dialog = await openedDrawer();

Expand All @@ -238,9 +243,7 @@ describe('Approvals drawer summary card — `hidden: true` trim (objectui#5565)'
});

it('COUNTER-PROBE: same fixture, same position, nothing declared hidden — it renders', async () => {
getObjectSchema.mockResolvedValue({
fields: { subject: { type: 'text' }, diagnosis_code: { type: 'text' } },
});
getObjectSchema.mockResolvedValue({ fields: EVERY_PAYLOAD_FIELD });
const dialog = await openedDrawer();

await waitFor(() => expect(getObjectSchema).toHaveBeenCalledWith(OBJECT));
Expand Down Expand Up @@ -277,7 +280,7 @@ describe('Approvals drawer lead amount — same card, same trim (objectui#5565)'

it('does not lead with an amount field the object declares hidden', async () => {
useAmountFixture();
getObjectSchema.mockResolvedValue({ fields: { total_amount: { hidden: true } } });
getObjectSchema.mockResolvedValue({ fields: { total_amount: { hidden: true }, subject: { type: 'text' } } });
const dialog = await openedDrawer();

await waitFor(() => expect(getObjectSchema).toHaveBeenCalledWith(OBJECT));
Expand All @@ -290,7 +293,7 @@ describe('Approvals drawer lead amount — same card, same trim (objectui#5565)'

it('COUNTER-PROBE: the same amount, undeclared, still leads the card', async () => {
useAmountFixture();
getObjectSchema.mockResolvedValue({ fields: { total_amount: {} } });
getObjectSchema.mockResolvedValue({ fields: { total_amount: {}, subject: { type: 'text' } } });
const dialog = await openedDrawer();

await waitFor(() => expect(getObjectSchema).toHaveBeenCalledWith(OBJECT));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
*
* ## The witnesses
*
* `created_by` and `organization_id` are in the page's `PAYLOAD_SYSTEM_KEYS`,
* `created_by` and `organization_id` are bookkeeping columns to the page's
* `isBookkeepingKey` (`@object-ui/types`' `isSystemManagedField`, objectui#11677),
* so the summary card already drops them: their values can reach the DOM only
* through the raw panel. That makes them exact witnesses for "the panel
* rendered", not proxies. (Both are also long enough to trip the summary's
Expand Down
Loading
Loading