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
7 changes: 7 additions & 0 deletions .changeset/12717-print-empty-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@object-ui/plugin-detail': patch
---

A record details section on a print page prints every field it declares, with no "Show N empty fields" button on paper (objectui#12717).

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.
17 changes: 15 additions & 2 deletions packages/plugin-detail/src/DetailSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,10 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
// draws open: Radix unmounts a closed section's content, so a section
// collapsed on screen never reaches paper, and `record:details` is in the
// printable block subset because it prints every field it declares. Off a
// print page the toggle is unchanged.
const isCollapsed = usePrintPage() === null && collapsedState;
// print page the toggle is unchanged. Read once, above every early return:
// the empty-field heuristic below reads the same answer (objectui#12717).
const isPrintPage = usePrintPage() !== null;
const isCollapsed = !isPrintPage && collapsedState;
const [copiedField, setCopiedField] = React.useState<string | null>(null);
const [visibleCount, setVisibleCount] = React.useState<number | undefined>(undefined);
const [showEmptyOverride, setShowEmptyOverride] = React.useState(false);
Expand Down Expand Up @@ -298,11 +300,22 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
// fields) and a lower empty ratio (≥25%) now trigger auto-hide so pages
// start dense by default rather than sparse. Mobile remains the most
// aggressive variant since vertical real estate is scarce.
//
// objectui#12717 — off on a page that declares `print`. The heuristic is a
// SCREEN density call nobody authored (it reads the viewport and a ratio),
// and its "Show N empty fields" toggle is a control that reached paper while
// the empty rows it hid did not. `record:details` is in the printable block
// subset because it prints every field it declares (objectui#11958), so on
// paper every declared row prints and, with nothing hidden, there is no
// toggle. ⛔ Not a `print:hidden` on the button: the rows would still be
// missing. The AUTHORED all-empty hide below is not gated: it is what the
// section declares, and it leaves no control behind.
const isMobile = useIsMobile();
const AUTO_HIDE_MIN_FIELDS = isMobile ? 3 : 4;
const AUTO_HIDE_RATIO = isMobile ? 0.2 : 0.25;
const filledCount = section.fields.length - emptyCount;
const shouldAutoHideEmpty =
!isPrintPage &&
!isEditing &&
section.fields.length >= AUTO_HIDE_MIN_FIELDS &&
emptyCount / section.fields.length >= AUTO_HIDE_RATIO &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* 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.
*/

/**
* objectui#12717 — on a page that declares `print`, a detail section prints
* every field it declares and no "Show N empty fields" toggle.
*
* The empty-field auto-hide is a screen density heuristic nobody authored. On
* paper it hid the empty rows and left its interactive toggle behind, while
* `record:details` is in the printable block subset because it prints every
* field it declares (objectui#11958). Under a `PrintPageProvider` the heuristic
* is off: every declared row draws and there is no toggle. The AUTHORED
* all-empty hide (`hideEmpty: true`) keeps doing what it declares on paper: the
* section draws nothing. Off a print page all of it is unchanged.
*/
import React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import { SchemaRenderer, PrintPageProvider } from '@object-ui/react';
// Module scope: importing the package index runs the registrations under test.
import '../index';

/** Four declared rows, two of them empty: the heuristic fires on screen. */
const sparseSection = {
type: 'detail-section',
title: 'Invoice Details',
fields: [
{ name: 'number', label: 'Number' },
{ name: 'customer', label: 'Customer' },
{ name: 'po_number', label: 'PO Number' },
{ name: 'notes', label: 'Notes' },
],
} as never;

const SPARSE_RECORD = { number: 'INV-001', customer: 'Acme', po_number: null, notes: '' };

const allEmptyHiddenSection = {
type: 'detail-section',
title: 'Shipping Address',
hideEmpty: true,
fields: [
{ name: 'street', label: 'Street' },
{ name: 'city', label: 'City' },
],
} as never;

const PRINT = { paperSize: 'A4' } as never;

const toggle = () => screen.queryByRole('button', { name: /empty fields/i });

afterEach(() => cleanup());

describe('a detail section on a print page prints every declared field (objectui#12717)', () => {
it('under a print page: no empty-field toggle, and every declared row draws, empty ones included', () => {
render(
<PrintPageProvider value={PRINT}>
<SchemaRenderer schema={sparseSection} data={SPARSE_RECORD} />
</PrintPageProvider>,
);
expect(toggle()).toBeNull();
expect(screen.queryByText(/empty fields/i)).toBeNull();
for (const label of ['Number', 'Customer', 'PO Number', 'Notes']) {
expect(screen.getByText(label)).toBeTruthy();
}
expect(screen.getAllByTitle('No value')).toHaveLength(2);
});

it('under a print page: an authored `hideEmpty: true` all-empty section draws nothing', () => {
const { container } = render(
<PrintPageProvider value={PRINT}>
<SchemaRenderer schema={allEmptyHiddenSection} data={{ street: null, city: '' }} />
</PrintPageProvider>,
);
expect(screen.queryByText('Shipping Address')).toBeNull();
expect(screen.queryByText('Street')).toBeNull();
expect(toggle()).toBeNull();
expect(container.textContent).toBe('');
});

it('CONTROL — off a print page the heuristic hides the empty rows behind the toggle', () => {
render(<SchemaRenderer schema={sparseSection} data={SPARSE_RECORD} />);
expect(toggle()?.textContent).toMatch(/Show 2 empty fields/);
expect(screen.getByText('Number')).toBeTruthy();
expect(screen.queryByText('PO Number')).toBeNull();
expect(screen.queryByText('Notes')).toBeNull();
});
});
Loading