Skip to content

Commit 2efa9fd

Browse files
authored
fix(detail+fields+app-shell): ADR-0085 #2548 follow-ups — strip title dedupe, group icon/description, currency channel, approvals Bearer (#2577)
UX follow-ups from the framework#2548 real-backend browser pass, re-verified live (14/14): highlight strip drops the record title (new resolveTitleField, both synth + RecordDetailView paths); fieldGroups icon/description reach detail sections (LazyIcon rendering); currencyConfig passes through field enrichment so spec-authored currency fields render their symbol; RecordMetaFooter actor-less fallback labels (+ zh mistranslation fix); select badges ellipsize with hover title; all approvals fetches (app-shell badge/inbox/record panel + console approvalsApi) send the Bearer token for split-origin deployments.
1 parent 887062c commit 2efa9fd

22 files changed

Lines changed: 282 additions & 23 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
'@object-ui/fields': patch
4+
'@object-ui/i18n': patch
5+
'@object-ui/app-shell': patch
6+
---
7+
8+
Detail-page UX follow-ups from the ADR-0085 PR4 real-backend browser pass (framework#2548):
9+
10+
- **Highlight strip no longer repeats the record title.** A declared
11+
`highlightFields` list containing the title field rendered it as the first
12+
chip — truncated — directly under the identical page H1. `deriveHighlightFields`
13+
now resolves the title (`primaryField` / `nameField` / deprecated
14+
`displayNameField`, else the conventional display-field names) via the new
15+
exported `resolveTitleField` and filters it from declared lists before the
16+
4-chip cap, matching what the heuristic branch always did. app-shell's
17+
`RecordDetailView` synthParts (which pre-computes the list and bypasses the
18+
derivation) applies the same filter.
19+
- **Per-field currency reaches the renderers.** The spec channel
20+
(`currencyConfig.defaultCurrency`) was dropped by the highlight-strip and
21+
detail-section field enrichment, so a spec-authored currency field could
22+
never show its symbol ("25,000,000" instead of "$25,000,000");
23+
`resolveFieldCurrency` reads it second after the designer-only bare
24+
`currency` key.
25+
- **app-shell approvals fetches send the Bearer token.** The header badge
26+
poll, home-inbox count, and record-page approvals panel were cookie-only
27+
(new shared `bearerAuthHeaders()` util) — same split-origin failure mode as
28+
the console `approvalsApi` fix below.
29+
- **`fieldGroups[].icon` / `description` reach detail pages.** The shared
30+
derivation (ADR-0085 §5) already passed them through; the detail synth
31+
dropped them. Sections now carry both, and `DetailSection` renders a real
32+
Lucide icon for identifier-shaped names (emoji/text values keep the
33+
historical text rendering).
34+
- **Record meta footer stops dangling without an actor.** Seeded/system rows
35+
with `created_by: null` rendered "Created by · 10m ago"; the footer now
36+
falls back to actor-less labels ("Created / Updated"), with new i18n keys in
37+
all six locales (and the zh `createdBy`/`updatedBy` mistranslation fixed:
38+
创建人/更新人, not 创建于/更新于).
39+
- **Select badges ellipsize instead of clipping mid-glyph.** In bounded
40+
containers (highlight-strip columns, grid cells) an overlong option label
41+
used to be cut at the container edge ("Technolog…"); badges now shrink with
42+
an inner truncate and expose the full label as a hover title. The highlight
43+
strip's hover title also prefers the option label over the raw stored value.
44+
45+
Console app (unversioned): `approvalsApi` now sends the stored Bearer token
46+
like every other console call — cookie-only auth silently lost the approvals
47+
surface on split-origin deployments where the SameSite cookie doesn't flow.

apps/console/src/services/approvalsApi.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
* Approvals REST helper.
33
*
44
* Thin fetch wrapper around the framework's approval endpoints
5-
* (`/api/v1/approvals/*`). Sends cookies for auth.
5+
* (`/api/v1/approvals/*`). Auth mirrors the rest of the console: the stored
6+
* Bearer token when present, plus cookies. Cookie-only auth silently lost
7+
* the approvals surface on split-origin deployments (custom-domain console ↔
8+
* API, or a dev console pointed at a remote backend) where the SameSite
9+
* cookie never flows — every other console call already sends the Bearer.
610
*
711
* Mirrors the shape exposed by `@objectstack/plugin-approvals` /
812
* `packages/rest/src/rest-server.ts`.
913
*/
14+
import { TokenStorage } from '@object-ui/auth';
1015

1116
const SERVER_URL = (import.meta.env.VITE_SERVER_URL || '').replace(/\/$/, '');
1217
const API_BASE = `${SERVER_URL}/api/v1`;
@@ -73,9 +78,14 @@ export interface ApprovalActionRow {
7378
}
7479

7580
async function call<T>(path: string, init?: RequestInit): Promise<T> {
81+
const token = TokenStorage.get();
7682
const res = await fetch(`${API_BASE}${path}`, {
7783
credentials: 'include',
78-
headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) },
84+
headers: {
85+
'Content-Type': 'application/json',
86+
...(token ? { Authorization: `Bearer ${token}` } : {}),
87+
...(init?.headers || {}),
88+
},
7989
...init,
8090
});
8191
let payload: any = null;

packages/app-shell/src/hooks/useHomeInbox.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { useEffect, useRef, useState } from 'react';
1818
import { useAdapter } from '../providers/AdapterProvider';
1919
import { useAuth } from '@object-ui/auth';
20+
import { bearerAuthHeaders } from '../utils/authToken';
2021
import type { ActivityItem } from '../layout/ActivityFeed';
2122

2223
export interface HomeNotification {
@@ -134,7 +135,11 @@ export function useHomeInbox(limit = 5): HomeInboxData {
134135
if (identities.length === 0) return;
135136
let cancelled = false;
136137
const qs = new URLSearchParams({ status: 'pending', approverId: identities.join(',') });
137-
fetch(`${serverUrl}/api/v1/approvals/requests?${qs}`, { credentials: 'include' })
138+
fetch(`${serverUrl}/api/v1/approvals/requests?${qs}`, {
139+
credentials: 'include',
140+
// Bearer too — see utils/authToken (#2548 split-origin fix).
141+
headers: bearerAuthHeaders(),
142+
})
138143
.then(async (res) => {
139144
if (!res.ok) return;
140145
const payload = await res.json().catch(() => null);

packages/app-shell/src/hooks/useRecordApprovals.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020

2121
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22+
import { bearerAuthHeaders } from '../utils/authToken';
2223

2324
export interface ApprovalRequestLite {
2425
id: string;
@@ -53,7 +54,13 @@ function apiBase() {
5354
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
5455
const res = await fetch(`${apiBase()}${path}`, {
5556
credentials: 'include',
56-
headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) },
57+
headers: {
58+
'Content-Type': 'application/json',
59+
// Bearer too — cookie-only auth loses this surface on split-origin
60+
// deployments where the SameSite cookie doesn't flow (#2548).
61+
...bearerAuthHeaders(),
62+
...(init?.headers || {}),
63+
},
5764
...init,
5865
});
5966
let payload: any = null;

packages/app-shell/src/layout/AppHeader.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { useAuth, getUserInitials, useIsWorkspaceAdmin } from '@object-ui/auth';
6868
import { useMetadata } from '../providers/MetadataProvider';
6969
import { resolveI18nLabel, preferLocal, matchAppBySegment, appRouteSegment, appStudioRoutePath } from '../utils';
7070
import { getIcon } from '../utils/getIcon';
71+
import { bearerAuthHeaders } from '../utils/authToken';
7172
import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
7273
import { useNavigationContext } from '../context/NavigationContext';
7374
import { useCommandPalette } from '../context/CommandPaletteProvider';
@@ -440,7 +441,11 @@ export function AppHeader({
440441
approvalsInFlightRef.current = true;
441442
try {
442443
const qs = new URLSearchParams({ status: 'pending', approverId: identities.join(',') });
443-
const res = await fetch(`${base}?${qs}`, { credentials: 'include' });
444+
const res = await fetch(`${base}?${qs}`, {
445+
credentials: 'include',
446+
// Bearer too — see utils/authToken (#2548 split-origin fix).
447+
headers: bearerAuthHeaders(),
448+
});
444449
if (res.status === 404) { approvalsUnavailableRef.current = true; return; }
445450
if (!res.ok) return;
446451
const payload = await res.json().catch(() => null);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
import { TokenStorage } from '@object-ui/auth';
10+
11+
/**
12+
* Bearer auth header from the console's stored session token
13+
* (`@object-ui/auth` TokenStorage — localStorage with in-memory fallback).
14+
*
15+
* For app-shell's few direct `fetch` call sites (approvals badge/panel,
16+
* home inbox): cookie-only fetches silently lose their surface on
17+
* split-origin deployments (custom-domain console ↔ API) where the
18+
* SameSite cookie never flows — every dataSource call already sends this
19+
* bearer, so these fetches must too (#2548).
20+
*/
21+
export function bearerAuthHeaders(): Record<string, string> {
22+
try {
23+
const token = TokenStorage.get();
24+
return token ? { Authorization: `Bearer ${token}` } : {};
25+
} catch {
26+
return {};
27+
}
28+
}

packages/app-shell/src/views/RecordDetailView.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
1212
import { useParams, useNavigate, useLocation, useSearchParams, Link } from 'react-router-dom';
13-
import { RecordChatterPanel, InlineEditSaveBar, buildDefaultPageSchema, deriveFieldGroupDetailSections, extractMentions } from '@object-ui/plugin-detail';
13+
import { RecordChatterPanel, InlineEditSaveBar, buildDefaultPageSchema, deriveFieldGroupDetailSections, extractMentions, resolveTitleField } from '@object-ui/plugin-detail';
1414
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
1515
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
1616
import { usePermissions } from '@object-ui/permissions';
@@ -1513,9 +1513,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
15131513
// to their name. Empty → undefined below, so the synthesizer
15141514
// auto-derives instead.
15151515
const rawHighlightFields = (objectDef as any).highlightFields ?? [];
1516+
// Drop the record's title field: it is the page H1 and repeating it as
1517+
// the first chip duplicated the name, truncated, right under the
1518+
// heading. Mirrors deriveHighlightFields' declared-list handling —
1519+
// this pre-computed list bypasses that derivation (#2548).
1520+
const titleField = resolveTitleField(objectDef as any);
15161521
const highlightFields: string[] = (Array.isArray(rawHighlightFields) ? rawHighlightFields : [])
15171522
.map((f: any) => (typeof f === 'string' ? f : f?.name))
1518-
.filter((n: any): n is string => typeof n === 'string' && n.length > 0);
1523+
.filter((n: any): n is string => typeof n === 'string' && n.length > 0 && n !== titleField);
15191524

15201525
// Related child lists from reverse-reference relationships, in
15211526
// `buildDefaultPageSchema`'s `related` shape. `relationshipField` is

packages/fields/src/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,13 +1050,18 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
10501050
}
10511051

10521052
const colorClasses = getBadgeColorClasses(option?.color, val);
1053+
// max-w-full + inner truncate: in bounded containers (detail highlight
1054+
// strip columns, grid cells) an overlong label used to clip mid-glyph at
1055+
// the container edge; now the badge shrinks and ellipsizes, with the full
1056+
// label on hover.
10531057
return (
10541058
<Badge
10551059
key={key}
10561060
variant="outline"
1057-
className={colorClasses}
1061+
className={cn('max-w-full min-w-0', colorClasses)}
1062+
title={label}
10581063
>
1059-
{label}
1064+
<span className="truncate">{label}</span>
10601065
</Badge>
10611066
);
10621067
};

packages/i18n/src/locales/de.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,8 @@ const de = {
510510
highlightFields: "Schlüsselfelder",
511511
createdBy: "Erstellt von",
512512
updatedBy: "Aktualisiert von",
513+
created: "Erstellt",
514+
updated: "Aktualisiert",
513515
showEmptyRelated_one: "+ {{count}} leer",
514516
showEmptyRelated_other: "+ {{count}} leer",
515517
copyEmail: "E-Mail kopieren",

packages/i18n/src/locales/en.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,9 +614,13 @@ const en = {
614614
// Activity feed actors
615615
systemActor: 'System',
616616
unknownUser: 'Unknown',
617-
// Record meta footer (audit provenance)
617+
// Record meta footer (audit provenance). `created`/`updated` are the
618+
// actor-less variants — "Created by · 5m ago" dangles when created_by
619+
// is null (system/seeded rows), so the footer falls back to these.
618620
createdBy: 'Created by',
619621
updatedBy: 'Updated by',
622+
created: 'Created',
623+
updated: 'Updated',
620624
// Attachments
621625
dropFilesToUpload: 'Drop files here or click to upload',
622626
attachmentCount: '{{count}} attachment',

0 commit comments

Comments
 (0)