From 8dc2a12bc379c3b77518eb5b892d62b667a186e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:55:35 +0000 Subject: [PATCH 1/2] fix(plugin-detail): a record on a print page does not print its approval band (objectui#12751) DetailView reads the print context once, above every early return, and does not draw the approval band (recall button, status badge, tally) on a page that declares print. Off a print page the band is unchanged. Co-authored-by: Claude --- .changeset/12751-print-approval-band.md | 9 ++ packages/plugin-detail/src/DetailView.tsx | 20 +++- ...etailView.printApprovalBand-12751.test.tsx | 100 ++++++++++++++++++ 3 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 .changeset/12751-print-approval-band.md create mode 100644 packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx diff --git a/.changeset/12751-print-approval-band.md b/.changeset/12751-print-approval-band.md new file mode 100644 index 0000000000..2aed085996 --- /dev/null +++ b/.changeset/12751-print-approval-band.md @@ -0,0 +1,9 @@ +--- +'@object-ui/plugin-detail': patch +--- + +A record on a print page no longer prints its approval band: no "Recall approval" button, no approval status badge and no approval tally on paper (objectui#12751). + +On a page that declares `print`, a record in approval put the screen's approval band on paper: the recall button, the "In approval · editable" or "Locked for approval" badge, and the "Approvals — 0 of 1" tally. The button is a control. The badge and the tally are the approval's live state, which differs by viewer and changes with each decision, the same reason a print page refuses `record:approvals` and `record:approval_decision`. On a print page the band is now not drawn, and the record's fields print as before. A printed record that should state its approval status prints the record's own status field through `record:details`. Off a print page the band is unchanged. + +Clause-②: no diff --git a/packages/plugin-detail/src/DetailView.tsx b/packages/plugin-detail/src/DetailView.tsx index 7897ce3261..97b55210cf 100644 --- a/packages/plugin-detail/src/DetailView.tsx +++ b/packages/plugin-detail/src/DetailView.tsx @@ -44,7 +44,7 @@ import { ActivityTimeline } from './ActivityTimeline'; import { HistoryTimeline } from './HistoryTimeline'; import { RecordMetaFooter } from './RecordMetaFooter'; import { RecordPageProvenanceContext } from './recordPageProvenance'; -import { SchemaRenderer, SchemaErrorBoundary, toRenderableSchema, useSafeFieldLabel, useDataInvalidation, useInlineEdit, useRowPredicate, resolveApprovalLock } from '@object-ui/react'; +import { SchemaRenderer, SchemaErrorBoundary, toRenderableSchema, useSafeFieldLabel, useDataInvalidation, useInlineEdit, useRowPredicate, resolveApprovalLock, usePrintPage } from '@object-ui/react'; import { buildExpandFields, declaredNameField, getRecordDisplayName, formatTitleTemplate, recordDisplayValueAt, resolveAffordance, toDisplayDate, userActionPredicates, withoutDeniedFields } from '@object-ui/core'; import { usePermissions } from '@object-ui/permissions'; import { useLocalization, useDisplayLocale, resolveFieldCurrency } from '@object-ui/i18n'; @@ -318,6 +318,11 @@ export const DetailView: React.FC = ({ // objectui#12307: `true` only under a `record:details` that reads the page's // record — the page host draws the one footer then. See the context's doc. const pageDrawsProvenance = React.useContext(RecordPageProvenanceContext); + // objectui#12751 — `true` on a page that declares `print`. Read once, above + // every early return, the way `DetailSection` reads it (objectui#12717): the + // approval band below is live, per-viewer workflow state, not document + // content, so it does not print. + const isPrintPage = usePrintPage() !== null; const isInlineEditing = inline?.editing ?? false; const editedValues = inline?.draft ?? EMPTY_DRAFT; const autoFocusField = inline?.autoFocusField ?? null; @@ -1632,8 +1637,17 @@ export const DetailView: React.FC = ({ (composed under a Lightning-style page:header), surface the running approval + recall affordance inline. The inline-edit Save / Cancel bar itself now lives in the record-level - (objectui#2407 P1). */} - {inlineEdit && schema.showHeader === false && (() => { + (objectui#2407 P1). + + objectui#12751 — not on a print page. The recall button is a control, + and the status badge and the tally are the approval's live state: + they differ by viewer (recall is the submitter's) and change with + each decision, which is why the spec refuses `record:approvals` and + `record:approval_decision` inside a print page. A printed record + that should say where its approval stands prints the record's own + status field through `record:details`, the way `record:path` leaves + the current stage to it. */} + {inlineEdit && schema.showHeader === false && !isPrintPage && (() => { // Two independent facts, not one (objectui#2902): // // isPending — an approval is running on this record. diff --git a/packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx b/packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx new file mode 100644 index 0000000000..3751d30a1c --- /dev/null +++ b/packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx @@ -0,0 +1,100 @@ +/** + * 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#12751 — on a page that declares `print`, the record's approval band + * does not print. + * + * Measured on the live printed PDF: a record in approval put its "Recall + * approval" button, its status badge and its "Approvals — 0 of 1" tally on + * paper. The button is a control. The badge and the tally are the approval's + * live state: they differ by viewer and change with each decision, the reason + * the spec refuses `record:approvals` and `record:approval_decision` inside a + * print page. So under a `PrintPageProvider` the whole band is gone, while the + * record's own fields still print. Off a print page the band is unchanged. + */ +import React from 'react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import { DetailView } from '../DetailView'; +import { InlineEditProvider, PrintPageProvider, type ApprovalProgress } from '@object-ui/react'; +import type { DetailViewSchema } from '@object-ui/types'; + +const RECALL = /Recall approval/; + +const schema: DetailViewSchema = { + type: 'detail-view', + title: 'Invoice', + objectName: 'invoice', + // The band renders only when DetailView's own header is suppressed, which is + // how `record:details` mounts it (`showHeader` defaults to false there). + showHeader: false, + data: { id: 'INV-1', name: 'INV-0001' }, + sections: [{ title: 'Basics', fields: [{ name: 'name', label: 'Name' }] }], +}; + +const PROGRESS: ApprovalProgress = { behavior: 'quorum', got: 0, need: 1 }; + +function makeDataSource() { + return { + find: vi.fn(async () => ({ data: [] })), + findOne: vi.fn(async () => null), + create: vi.fn(async () => ({})), + update: vi.fn(async () => ({})), + delete: vi.fn(async () => ({})), + getObjectSchema: vi.fn(async () => null), + cancelPendingApproval: vi.fn(async () => ({ requestId: 'r1', status: 'recalled' })), + } as any; +} + +function renderRecord(locked: boolean, onPrintPage: boolean) { + const view = ( + + + + ); + return render(onPrintPage ? {view} : view); +} + +afterEach(() => cleanup()); + +describe('the approval band does not print (objectui#12751)', () => { + it.each([ + ['an editable approval', false], + ['a locking approval', true], + ])('under a print page, %s draws no recall button, no status badge and no tally', (_label, locked) => { + renderRecord(locked, true); + + // The record itself still prints, so the absences below measure the band. + expect(screen.getByText('INV-0001')).toBeInTheDocument(); + + expect(screen.queryByRole('button', { name: RECALL })).toBeNull(); + expect(screen.queryByRole('status')).toBeNull(); + expect(screen.queryByRole('progressbar')).toBeNull(); + expect(screen.queryByText('In approval · editable')).toBeNull(); + expect(screen.queryByText('Locked for approval')).toBeNull(); + }); + + it.each([ + ['an editable approval', false, 'In approval · editable'], + ['a locking approval', true, 'Locked for approval'], + ])('CONTROL — off a print page, %s draws the band unchanged', (_label, locked, badge) => { + renderRecord(locked, false); + + expect(screen.getByText('INV-0001')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: RECALL })).toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent(badge); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); +}); From 34077bf89984661907c1ad3d32ef14c235c006d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:07:57 +0000 Subject: [PATCH 2/2] test(plugin-detail): type the print approval band test's data source double (objectui#12751) Co-authored-by: Claude --- .../src/__tests__/DetailView.printApprovalBand-12751.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx b/packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx index 3751d30a1c..a8bea3a81e 100644 --- a/packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx +++ b/packages/plugin-detail/src/__tests__/DetailView.printApprovalBand-12751.test.tsx @@ -23,7 +23,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import { DetailView } from '../DetailView'; import { InlineEditProvider, PrintPageProvider, type ApprovalProgress } from '@object-ui/react'; -import type { DetailViewSchema } from '@object-ui/types'; +import type { DetailViewSchema, DataSource } from '@object-ui/types'; const RECALL = /Recall approval/; @@ -49,7 +49,7 @@ function makeDataSource() { delete: vi.fn(async () => ({})), getObjectSchema: vi.fn(async () => null), cancelPendingApproval: vi.fn(async () => ({ requestId: 'r1', status: 'recalled' })), - } as any; + } as unknown as DataSource; } function renderRecord(locked: boolean, onPrintPage: boolean) {