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
9 changes: 9 additions & 0 deletions .changeset/12751-print-approval-band.md
Original file line number Diff line number Diff line change
@@ -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
20 changes: 17 additions & 3 deletions packages/plugin-detail/src/DetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -318,6 +318,11 @@ export const DetailView: React.FC<DetailViewProps> = ({
// 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;
Expand Down Expand Up @@ -1632,8 +1637,17 @@ export const DetailView: React.FC<DetailViewProps> = ({
(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 <InlineEditSaveBar>
(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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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, DataSource } 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 unknown as DataSource;
}

function renderRecord(locked: boolean, onPrintPage: boolean) {
const view = (
<InlineEditProvider
canEdit={false}
locked={locked}
approvalPending
approvalIsSubmitter
approvalProgress={PROGRESS}
>
<DetailView schema={schema} dataSource={makeDataSource()} inlineEdit />
</InlineEditProvider>
);
return render(onPrintPage ? <PrintPageProvider value={{ paperSize: 'A4' }}>{view}</PrintPageProvider> : 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();
});
});
Loading