Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 263e9dd

Browse files
fix(plugin-detail): a detail section on a print page prints every declared field and no "Show N empty fields" button (#12729)
Fixes #12717 Clause-②: no ## What On a page that declares `print`, a `record:details` section prints every field it declares. The "Show N empty fields" / "Hide empty fields" button no longer reaches paper. `packages/plugin-detail/src/DetailSection.tsx` hides empty fields for two reasons, and they belong to two different owners. - `shouldAutoHideEmpty` is the renderer's own density heuristic. No author declares it, and it depends on the viewport (`useIsMobile`) and on a ratio of empty fields, so it is a screen concern. On a print page it is now off. Every declared field prints, an empty one as the shared placeholder dash. The toggle is derived from the heuristic (`showEmptyToggle = emptyCount > 0 && shouldAutoHideEmpty`), so with no auto-hide there is no toggle. This follows card ②'s rule (objectui#11958): `record:details` is in the printable block subset because it prints every field it declares. That is the same reason the collapsed section already opens on paper. - `hideAllEmptySection` (`section.hideEmpty === true`) is the authored `record:details` key. On paper it keeps doing what it declares: an all-empty section prints nothing at all, with no heading and no control. It is not gated. `usePrintPage()` is now read once, above every early return (`isPrintPage`). The existing collapse gate and the new heuristic gate both read that one value. Not done, deliberately: - No `print:hidden` on the button. The empty fields would still be missing from the paper. - No change to `page-print.ts`. - The gate stays local to the heuristic, so objectui#12618 (the on-screen all-empty fold, on hold) can change that fold and keep this gate. Off a print page, the toggle, the heuristic and the authored hide are unchanged. ## Tests `packages/plugin-detail/src/__tests__/detailSectionPrintEmptyToggle-12717.test.tsx`, all at head 9f28cd2: 1. Inside a `PrintPageProvider`: no toggle, all four declared fields present, two placeholders. 2. Inside a `PrintPageProvider`: an authored `hideEmpty: true` all-empty section renders nothing. 3. Control, off a print page: the toggle reads "Show 2 empty fields" and the empty rows are hidden. Each test was proven red by a one-off ablation, then restored. Each restore was checked: blob 576f471 equals HEAD and `git diff HEAD` is empty. - Deleting the print gate turns test 1 red. - Gating the authored hide on print (the wrong fix) turns test 2 red. - Turning the heuristic off everywhere turns test 3 red. `pnpm exec vitest run packages/plugin-detail` at 9f28cd2: 275 files passed, 1 skipped; 2663 tests passed, 14 skipped. ## Local gates (9f28cd2) All of these exited 0: - the closure build for `@object-ui/plugin-detail`; - the `@object-ui/plugin-detail` build and type-check; - the console build with `VITE_BASE_PATH=/console/`; - `check:changeset-claims`, `changeset:check`, `check:test-path-roots`, `check:spec-symbols` and the changeset presence check. eslint on the changed files reports 0 errors. Its 12 warnings were all in `DetailSection.tsx` already; the base file has the same 12. ## Acceptance notes - NOT MEASURED: PDF reading. The showcase invoice print page `showcase_invoice_document` is not in the published showcase at `@objectstack/cli@18.0.0-next.2`, nor in objectstack `main` 6d1660d. As a fallback, I added a throwaway page-level `print` to a showcase record page in a scratch copy of the backend. It never reached the served page metadata, and no print page mounted on the record route, so no before/after PDF could be rendered. - Other controls in this file on paper: - A collapsible section's disclosure chevron still prints, inside the header button that card ② opens. It is outside this claim's file surface, so it is reported, not fixed. - The hover copy and pencil buttons are invisible until hover, so they draw nothing on paper. Provenance: built by the cloud session `session_01D7HiWiz4iSX26uyJLB8jNs` (claim `6218362787`) and opened by the `domain:ui` seat 1 PM (`session_011K9g23W67dy2zwnyzaoB6o`) from that session's final message, because a cloud session cannot open a PR from its container. The seat replaced the dev's own footer lines with this line. --- _Generated by [Claude Code](https://claude.ai/code/session_01D7HiWiz4iSX26uyJLB8jNs)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent c41ce3f commit 263e9dd

3 files changed

Lines changed: 114 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
---
4+
5+
A record details section on a print page prints every field it declares, with no "Show N empty fields" button on paper (objectui#12717).
6+
7+
On screen, a section that has some empty fields hides them and offers a "Show N empty fields" toggle. That density rule is the renderer's own, not something the page declares, and on a page that declares `print` it put the toggle on paper while the empty fields stayed off it. On a print page the rule is now off: every declared field prints, an empty one as its placeholder dash, and there is no toggle. A section authored with `hideEmpty: true` whose fields are all empty still prints nothing, as it declares. Off a print page nothing changes.

‎packages/plugin-detail/src/DetailSection.tsx‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,10 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
184184
// draws open: Radix unmounts a closed section's content, so a section
185185
// collapsed on screen never reaches paper, and `record:details` is in the
186186
// printable block subset because it prints every field it declares. Off a
187-
// print page the toggle is unchanged.
188-
const isCollapsed = usePrintPage() === null && collapsedState;
187+
// print page the toggle is unchanged. Read once, above every early return:
188+
// the empty-field heuristic below reads the same answer (objectui#12717).
189+
const isPrintPage = usePrintPage() !== null;
190+
const isCollapsed = !isPrintPage && collapsedState;
189191
const [copiedField, setCopiedField] = React.useState<string | null>(null);
190192
const [visibleCount, setVisibleCount] = React.useState<number | undefined>(undefined);
191193
const [showEmptyOverride, setShowEmptyOverride] = React.useState(false);
@@ -298,11 +300,22 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
298300
// fields) and a lower empty ratio (≥25%) now trigger auto-hide so pages
299301
// start dense by default rather than sparse. Mobile remains the most
300302
// aggressive variant since vertical real estate is scarce.
303+
//
304+
// objectui#12717 — off on a page that declares `print`. The heuristic is a
305+
// SCREEN density call nobody authored (it reads the viewport and a ratio),
306+
// and its "Show N empty fields" toggle is a control that reached paper while
307+
// the empty rows it hid did not. `record:details` is in the printable block
308+
// subset because it prints every field it declares (objectui#11958), so on
309+
// paper every declared row prints and, with nothing hidden, there is no
310+
// toggle. ⛔ Not a `print:hidden` on the button: the rows would still be
311+
// missing. The AUTHORED all-empty hide below is not gated: it is what the
312+
// section declares, and it leaves no control behind.
301313
const isMobile = useIsMobile();
302314
const AUTO_HIDE_MIN_FIELDS = isMobile ? 3 : 4;
303315
const AUTO_HIDE_RATIO = isMobile ? 0.2 : 0.25;
304316
const filledCount = section.fields.length - emptyCount;
305317
const shouldAutoHideEmpty =
318+
!isPrintPage &&
306319
!isEditing &&
307320
section.fields.length >= AUTO_HIDE_MIN_FIELDS &&
308321
emptyCount / section.fields.length >= AUTO_HIDE_RATIO &&
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#12717 — on a page that declares `print`, a detail section prints
11+
* every field it declares and no "Show N empty fields" toggle.
12+
*
13+
* The empty-field auto-hide is a screen density heuristic nobody authored. On
14+
* paper it hid the empty rows and left its interactive toggle behind, while
15+
* `record:details` is in the printable block subset because it prints every
16+
* field it declares (objectui#11958). Under a `PrintPageProvider` the heuristic
17+
* is off: every declared row draws and there is no toggle. The AUTHORED
18+
* all-empty hide (`hideEmpty: true`) keeps doing what it declares on paper: the
19+
* section draws nothing. Off a print page all of it is unchanged.
20+
*/
21+
import React from 'react';
22+
import { describe, it, expect, afterEach } from 'vitest';
23+
import { render, screen, cleanup } from '@testing-library/react';
24+
import { SchemaRenderer, PrintPageProvider } from '@object-ui/react';
25+
// Module scope: importing the package index runs the registrations under test.
26+
import '../index';
27+
28+
/** Four declared rows, two of them empty: the heuristic fires on screen. */
29+
const sparseSection = {
30+
type: 'detail-section',
31+
title: 'Invoice Details',
32+
fields: [
33+
{ name: 'number', label: 'Number' },
34+
{ name: 'customer', label: 'Customer' },
35+
{ name: 'po_number', label: 'PO Number' },
36+
{ name: 'notes', label: 'Notes' },
37+
],
38+
} as never;
39+
40+
const SPARSE_RECORD = { number: 'INV-001', customer: 'Acme', po_number: null, notes: '' };
41+
42+
const allEmptyHiddenSection = {
43+
type: 'detail-section',
44+
title: 'Shipping Address',
45+
hideEmpty: true,
46+
fields: [
47+
{ name: 'street', label: 'Street' },
48+
{ name: 'city', label: 'City' },
49+
],
50+
} as never;
51+
52+
const PRINT = { paperSize: 'A4' } as never;
53+
54+
const toggle = () => screen.queryByRole('button', { name: /empty fields/i });
55+
56+
afterEach(() => cleanup());
57+
58+
describe('a detail section on a print page prints every declared field (objectui#12717)', () => {
59+
it('under a print page: no empty-field toggle, and every declared row draws, empty ones included', () => {
60+
render(
61+
<PrintPageProvider value={PRINT}>
62+
<SchemaRenderer schema={sparseSection} data={SPARSE_RECORD} />
63+
</PrintPageProvider>,
64+
);
65+
expect(toggle()).toBeNull();
66+
expect(screen.queryByText(/empty fields/i)).toBeNull();
67+
for (const label of ['Number', 'Customer', 'PO Number', 'Notes']) {
68+
expect(screen.getByText(label)).toBeTruthy();
69+
}
70+
expect(screen.getAllByTitle('No value')).toHaveLength(2);
71+
});
72+
73+
it('under a print page: an authored `hideEmpty: true` all-empty section draws nothing', () => {
74+
const { container } = render(
75+
<PrintPageProvider value={PRINT}>
76+
<SchemaRenderer schema={allEmptyHiddenSection} data={{ street: null, city: '' }} />
77+
</PrintPageProvider>,
78+
);
79+
expect(screen.queryByText('Shipping Address')).toBeNull();
80+
expect(screen.queryByText('Street')).toBeNull();
81+
expect(toggle()).toBeNull();
82+
expect(container.textContent).toBe('');
83+
});
84+
85+
it('CONTROL — off a print page the heuristic hides the empty rows behind the toggle', () => {
86+
render(<SchemaRenderer schema={sparseSection} data={SPARSE_RECORD} />);
87+
expect(toggle()?.textContent).toMatch(/Show 2 empty fields/);
88+
expect(screen.getByText('Number')).toBeTruthy();
89+
expect(screen.queryByText('PO Number')).toBeNull();
90+
expect(screen.queryByText('Notes')).toBeNull();
91+
});
92+
});

0 commit comments

Comments
 (0)