Skip to content

Commit cbb3a1a

Browse files
fix(console): the approvals inbox names the flow, the step and the object in the reader's language (#27040)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8NwmMmPzpAXtxV3vdWEazA3)_ Fixes #26883 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:审批收件箱的所有审批接口调用改走控制台的认证 fetch(和申请详情页同一条通道),请求带上用户选择的界面语言;对象筛选下拉显示对象标签而不是机器名。 - **为什么改**:服务端审批接口早已按 Accept-Language 从应用语言包翻译流程/步骤/对象标签,但收件箱用裸 fetch,只带浏览器语言,所以中文用户在收件箱看到英文。 - **风险与回滚**:低;只换了传输层(额外带上 X-Tenant-ID 与界面语言头,和详情页一致),回滚即还原 `approvalsApi.ts` 的 `call`。 - **先看**:`ui/apps/console/src/services/approvalsApi.ts` 的 `call`;`ApprovalsInboxPage.tsx` 的 `objectOptions`;新测试 `ApprovalsInboxPage.readerLocaleLabels-26883.test.tsx`。 - **两分钟验证**:`pnpm --dir ui exec vitest run apps/console/src/pages/system/ApprovalsInboxPage.readerLocaleLabels-26883.test.tsx`,预期 6 个全过。 - **需要你做的**:无,席位落地;Setup › 审批申请 列表那一半走服务端,另立卡(见 Acceptance notes)。 ## What changes - `approvalsApi.call` (every inbox read and decision) now goes through `createAuthenticatedFetch()` from `@object-ui/auth`, the transport `approvalRequestsDataSource.ts` (the request page) already uses. It stamps the Bearer (as before), `X-Tenant-ID`, and the active UI language as `Accept-Language`. The hand-built Bearer header is gone because the wrapper sets it. - The approvals door already serves `process_label`, `step_label` and `object_label` in the request's `Accept-Language` from the app's pack (`flows.FLOW.label`, `flows.FLOW.approvals.NODE.label`, `objects.OBJECT.label`; `reader-locale-labels.ts` in `@objectstack/plugin-approvals`, shipped in 18.1.0), keeping the stored label when the pack has no entry. So the row, the process filter and the object column now show the pack's text with no client-side resolver. - Bounded in-place fix, same defect class and same file: the inbox's object filter listed machine names (`opportunity`); it now lists each object by the label the row serves, and the search haystack includes `object_label`. - Changeset: `@object-ui/console` patch. ## Why The card's mechanism assumption (the inbox shows the stored English with no lookup, fix by a client lookup) is disproved on `origin/main` `5e99bd93`: the server translates per reader, and the inbox simply never told it the reader's language. A bare `fetch` sends only the browser's language, so a zh-CN user on an en-US browser got English. Same root cause and same fix shape as the `/forms/:name` select-option fix (`.changeset/26452-form-options-locale.md`). No second resolver, nothing stored changes, no server change. ## Tests New `ui/apps/console/src/pages/system/ApprovalsInboxPage.readerLocaleLabels-26883.test.tsx`: the real `I18nProvider` writes the html element's `lang` attribute, the real `createAuthenticatedFetch` reads it, and a global `fetch` stand-in for the door answers in the language it is asked for. - the inbox asks the door with `Accept-Language: zh` under a zh UI; - the queue row shows 大额商机审批 / 销售经理审核 / 商机, not the English; - a flow the pack does not cover keeps its stored labels; - the process and object filters list the shown labels; - search matches the shown text and the machine name; - control: an English UI reads the stored English. ## Acceptance notes - **Out of scope, list half:** Setup › Approval Requests is the generic `sys_approval_request` object list (`nav_approval_requests` in `approvals-plugin.ts`), read over the generic data door, which serves the stored `process_label` / `current_step_label` (the `reader-locale-labels.ts` header says so). The fix lands server-side in `@objectstack/plugin-approvals`, not in `plugin-list` / `plugin-grid`, filed as #27041. - **Premise drift:** the inbox's detail sheet named in the dispatch was deleted earlier; opening a request goes to the served request page, which already reads through the authenticated fetch. - **Search:** the stored English is no longer on the wire for a translated row, so search matches the shown (translated) text and the machine names, not the stored English. ## Verification <details><summary>Ablation</summary> Commit `120f45fd`. `node scripts/ablation-replace.mjs` replaced `const res = await createAuthenticatedFetch()(` with `const res = await fetch(` in `approvalsApi.ts` (anchor 1 to 0, blob `5ada17474e93` to `473dc3b025d1`), ran the new file: 5 failed, 1 passed (the untranslated-flow fallback, which is expected to hold either way). Restored: blob equals HEAD, `git diff HEAD` empty. </details> <details><summary>Local gates</summary> Head `2c07fcd0` (after merging `origin/main` `7eb613c3`). `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 33 families; all 33 ran, every one exit 0; `--ran` reconciliation: 33 derived, 33 run, 0 NOT-MEASURED. Package checks on the same head: `pnpm --dir ui exec vitest run apps/console/src/pages/system/ApprovalsInbox apps/console/src/services/approval apps/console/src/__tests__/approvalsInboxComponentRef.test.tsx`: Test Files 29 passed, Tests 204 passed. `pnpm --filter @object-ui/console type-check`: exit 0. ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (CI-owned). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbYXpuN8KEvNpYF9hQG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6aba7cb commit cbb3a1a

4 files changed

Lines changed: 281 additions & 10 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/console': patch
3+
---
4+
5+
fix(console): the approvals inbox names the flow, the step and the object in the language the user chose
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** A user whose chosen language differed from the browser's (a zh-CN user on an en-US browser) read every inbox row's flow, step and object in the authoring language, while the request page showed the same request translated. The inbox read the approvals routes with a bare `fetch`, so the approvals door picked its labels by the browser's `Accept-Language`.
11+
- **Now.** Every approvals call the inbox makes goes through the console's authenticated fetch, the channel the request page already reads over. It sends the session's language as `Accept-Language`, and the door answers `process_label`, `step_label` and `object_label` from the app's pack (`flows.FLOW.label`, `flows.FLOW.approvals.NODE.label`, `objects.OBJECT.label`). A flow, step or object the pack does not cover keeps its stored label. No second translator is added. The inbox's object filter now lists each object by that label instead of its machine name, and search matches the object label too.
12+
- **Unchanged.** What is stored on the request row. The Setup › Approval Requests object list reads the generic data door, which serves the stored labels.
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
* The approvals inbox names the flow, the step and the object in the reader's
11+
* UI language (objectstack#26883).
12+
*
13+
* The approvals door serves `process_label`, `step_label` and `object_label`
14+
* in the language the request's `Accept-Language` names, from the app's pack
15+
* (`flows.<flow>.label`, `flows.<flow>.approvals.<node_id>.label`,
16+
* `objects.<object>.label`), and the stored authoring text when the pack has
17+
* no entry. The request page reads through the console's authenticated fetch,
18+
* which stamps the active UI language; the inbox's `approvalsApi` called a
19+
* bare `fetch`, which carries only the browser's language, so a reader who
20+
* switched the console to 中文 read the inbox in English.
21+
*
22+
* Nothing between the page and the wire is stubbed here: the real
23+
* `I18nProvider` writes `<html lang>`, the real `createAuthenticatedFetch`
24+
* reads it, and the global `fetch` below stands in for the door, answering in
25+
* the language it is asked for, as the server does. A page that does not send
26+
* the UI language gets the English labels and goes red.
27+
*/
28+
29+
import '@testing-library/jest-dom/vitest';
30+
import { useEffect, useState, type ReactNode } from 'react';
31+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
32+
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
33+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
34+
import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions';
35+
import { I18nProvider } from '@object-ui/i18n';
36+
37+
const { ADAPTER, AUTH } = vi.hoisted(() => ({
38+
ADAPTER: {
39+
find: async () => ({ data: [{ id: 'opp_1' }, { id: 'exp_1' }] }),
40+
getObjectSchema: async () => ({ fields: {} }),
41+
},
42+
AUTH: { user: { id: 'u_1', email: 'approver@example.com' } },
43+
}));
44+
45+
vi.mock('@object-ui/auth', async (importOriginal) => ({
46+
...(await importOriginal<Record<string, unknown>>()),
47+
useAuth: () => AUTH,
48+
}));
49+
50+
vi.mock('@object-ui/app-shell', async (importOriginal) => ({
51+
...(await importOriginal<Record<string, unknown>>()),
52+
useAdapter: () => ADAPTER,
53+
useMetadata: () => ({ objects: [] }),
54+
DeclaredActionsBar: () => null,
55+
isViaOverrideRow: () => false,
56+
}));
57+
58+
import { ApprovalsInboxPage } from './ApprovalsInboxPage';
59+
60+
/** The app's zh-CN pack, as the door would read it; `expense_approval` has no entry. */
61+
const ZH_PACK: Record<string, string> = {
62+
'flows.opportunity_approval.label': '大额商机审批',
63+
'flows.opportunity_approval.approvals.manager_review.label': '销售经理审核',
64+
'objects.opportunity.label': '商机',
65+
};
66+
67+
function row(id: string, flow: string, node: string, object: string, stored: { process: string; step: string; object: string }) {
68+
return {
69+
id,
70+
process_name: `flow:${flow}`,
71+
process_label: stored.process,
72+
current_step: node,
73+
step_label: stored.step,
74+
object_name: object,
75+
object_label: stored.object,
76+
record_id: id,
77+
record_title: `Record ${id}`,
78+
status: 'pending',
79+
pending_approvers: ['u_1'],
80+
submitter_id: 'u_2',
81+
submitter_name: 'Sam Submitter',
82+
submitted_at: new Date().toISOString(),
83+
};
84+
}
85+
86+
/** The approvals door: each label in the asked language when the pack has it, else as stored. */
87+
function doorRows(acceptLanguage: string | null) {
88+
const zh = /^zh\b/i.test(acceptLanguage ?? '');
89+
const pick = (key: string, stored: string) => (zh && ZH_PACK[key]) || stored;
90+
return [
91+
row('opp_1', 'opportunity_approval', 'manager_review', 'opportunity', {
92+
process: pick('flows.opportunity_approval.label', 'Large Deal Approval'),
93+
step: pick('flows.opportunity_approval.approvals.manager_review.label', 'Sales Manager Review'),
94+
object: pick('objects.opportunity.label', 'Opportunity'),
95+
}),
96+
row('exp_1', 'expense_approval', 'finance_review', 'expense', {
97+
process: pick('flows.expense_approval.label', 'Expense Approval'),
98+
step: pick('flows.expense_approval.approvals.finance_review.label', 'Finance Review'),
99+
object: pick('objects.expense.label', 'Expense'),
100+
}),
101+
];
102+
}
103+
104+
let seenLanguages: Array<string | null> = [];
105+
106+
beforeEach(() => {
107+
seenLanguages = [];
108+
document.documentElement.lang = '';
109+
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
110+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
111+
const lang = new Headers(init?.headers).get('Accept-Language');
112+
if (/\/approvals\/requests(\?|$)/.test(url)) {
113+
seenLanguages.push(lang);
114+
const data = doorRows(lang);
115+
return new Response(JSON.stringify({ data, total: data.length }), { status: 200 });
116+
}
117+
return new Response(JSON.stringify({ data: [] }), { status: 200 });
118+
}));
119+
});
120+
121+
afterEach(() => {
122+
cleanup();
123+
vi.unstubAllGlobals();
124+
});
125+
126+
function permissions(): MePermissionsResponse {
127+
return {
128+
authenticated: true,
129+
userId: 'u_1',
130+
tenantId: 't_1',
131+
roles: [],
132+
permissionSets: [],
133+
objects: {},
134+
fields: {},
135+
systemPermissions: ['setup.access'],
136+
};
137+
}
138+
139+
function renderInbox(language: string) {
140+
return render(
141+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }} persistLanguage={false}>
142+
<MePermissionsProvider initialPermissions={permissions()}>
143+
<MemoryRouter initialEntries={['/apps/app/system/approvals']}>
144+
<Routes>
145+
<Route path="/apps/:appName/system/approvals" element={<AfterBoot><ApprovalsInboxPage /></AfterBoot>} />
146+
</Routes>
147+
</MemoryRouter>
148+
</MePermissionsProvider>
149+
</I18nProvider>,
150+
);
151+
}
152+
153+
/**
154+
* Mounts its children one commit after the providers. The console mounts a
155+
* page behind its boot (auth, routing), long after `I18nProvider` has written
156+
* `<html lang>`; a page mounted in the provider's own first commit would run
157+
* its fetch effect before the provider's (children's effects run first).
158+
*/
159+
function AfterBoot({ children }: { children: ReactNode }) {
160+
const [booted, setBooted] = useState(false);
161+
useEffect(() => setBooted(true), []);
162+
return booted ? <>{children}</> : null;
163+
}
164+
165+
/** The queue row (table row) that holds `recordTitle`. */
166+
async function queueRow(recordTitle: string): Promise<HTMLElement> {
167+
return waitFor(() => {
168+
const tr = screen.getAllByText(recordTitle)
169+
.map((el) => el.closest('tr'))
170+
.find((el): el is HTMLTableRowElement => !!el);
171+
expect(tr, `no table row holds ${recordTitle}`).toBeTruthy();
172+
return tr!;
173+
});
174+
}
175+
176+
describe('ApprovalsInboxPage — flow, step and object labels in the reader language (objectstack#26883)', () => {
177+
it('asks the approvals door in the active UI language', async () => {
178+
renderInbox('zh');
179+
await queueRow('Record opp_1');
180+
expect(seenLanguages.length).toBeGreaterThan(0);
181+
expect(seenLanguages.every((l) => l === 'zh'), `saw: ${JSON.stringify(seenLanguages)}`).toBe(true);
182+
});
183+
184+
it('the queue row shows the pack\'s flow, step and object labels under zh', async () => {
185+
renderInbox('zh');
186+
const tr = await queueRow('Record opp_1');
187+
expect(within(tr).getByText('大额商机审批')).toBeInTheDocument();
188+
expect(within(tr).getByText('销售经理审核')).toBeInTheDocument();
189+
expect(within(tr).getByText('商机')).toBeInTheDocument();
190+
expect(within(tr).queryByText('Opportunity')).toBeNull();
191+
expect(within(tr).queryByText('Large Deal Approval')).toBeNull();
192+
expect(within(tr).queryByText('Sales Manager Review')).toBeNull();
193+
});
194+
195+
it('a flow the pack does not translate keeps its stored labels', async () => {
196+
renderInbox('zh');
197+
const tr = await queueRow('Record exp_1');
198+
expect(within(tr).getByText('Expense Approval')).toBeInTheDocument();
199+
expect(within(tr).getByText('Finance Review')).toBeInTheDocument();
200+
});
201+
202+
it('the process and object filters list the labels shown', async () => {
203+
renderInbox('zh');
204+
await queueRow('Record opp_1');
205+
const triggers = screen.getAllByRole('combobox');
206+
const texts: string[] = [];
207+
for (const trigger of triggers) {
208+
fireEvent.click(trigger);
209+
for (const option of screen.queryAllByRole('option')) texts.push((option.textContent ?? '').trim());
210+
fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' });
211+
cleanupOpenListbox();
212+
}
213+
expect(texts, `options: ${JSON.stringify(texts)}`).toEqual(expect.arrayContaining(['大额商机审批', 'Expense Approval', '商机', 'Expense']));
214+
expect(texts).not.toContain('Large Deal Approval');
215+
expect(texts).not.toContain('opportunity');
216+
});
217+
218+
it('search matches the shown text and the machine name', async () => {
219+
renderInbox('zh');
220+
await queueRow('Record opp_1');
221+
const search = screen.getByRole('textbox');
222+
fireEvent.change(search, { target: { value: '销售经理' } });
223+
expect(screen.queryAllByText('Record opp_1').length).toBeGreaterThan(0);
224+
expect(screen.queryAllByText('Record exp_1')).toHaveLength(0);
225+
fireEvent.change(search, { target: { value: '商机' } });
226+
expect(screen.queryAllByText('Record opp_1').length).toBeGreaterThan(0);
227+
fireEvent.change(search, { target: { value: 'opportunity_approval' } });
228+
expect(screen.queryAllByText('Record opp_1').length).toBeGreaterThan(0);
229+
expect(screen.queryAllByText('Record exp_1')).toHaveLength(0);
230+
});
231+
232+
it('control: an English UI reads the stored English labels', async () => {
233+
renderInbox('en');
234+
const tr = await queueRow('Record opp_1');
235+
expect(within(tr).getByText('Large Deal Approval')).toBeInTheDocument();
236+
expect(within(tr).getByText('Sales Manager Review')).toBeInTheDocument();
237+
expect(seenLanguages.every((l) => l === 'en'), `saw: ${JSON.stringify(seenLanguages)}`).toBe(true);
238+
});
239+
});
240+
241+
/** Radix Select keeps a closed listbox mounted for a tick; drop any left so the next trigger lists only its own options. */
242+
function cleanupOpenListbox(): void {
243+
for (const el of document.querySelectorAll('[role="listbox"]')) el.remove();
244+
}

‎ui/apps/console/src/pages/system/ApprovalsInboxPage.tsx‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -949,11 +949,16 @@ function ApprovalsQueue() {
949949
return Array.from(set).sort();
950950
}, [rows]);
951951

952-
/** Unique object names present in current rows (for filter dropdown). */
952+
/**
953+
* Unique objects present in current rows (for filter dropdown): filtered by
954+
* machine name, shown by the label the row serves, which the approvals door
955+
* picks in the reader's language (objectstack#26883).
956+
*/
953957
const objectOptions = useMemo(() => {
954-
const set = new Set<string>();
955-
for (const r of rows) if (r.object_name) set.add(r.object_name);
956-
return Array.from(set).sort();
958+
const byName = new Map<string, string>();
959+
for (const r of rows) if (r.object_name && !byName.has(r.object_name)) byName.set(r.object_name, objectDisplay(r));
960+
return Array.from(byName, ([value, label]) => ({ value, label }))
961+
.sort((a, b) => a.label.localeCompare(b.label));
957962
}, [rows]);
958963

959964
/** Client-side filtered + sorted rows shown in table. */
@@ -969,7 +974,7 @@ function ApprovalsQueue() {
969974
if (tab !== 'pending') return true;
970975
if (!q) return true;
971976
const hay = [
972-
r.process_name, r.process_label, r.step_label, r.object_name,
977+
r.process_name, r.process_label, r.step_label, r.object_name, r.object_label,
973978
r.record_id, r.record_title, r.submitter_id, r.submitter_name,
974979
...(r.pending_approvers || []),
975980
].filter(Boolean).join(' ').toLowerCase();
@@ -1301,7 +1306,7 @@ function ApprovalsQueue() {
13011306
<SelectContent>
13021307
<SelectItem value="all">{tr('allObjects', 'All objects')}</SelectItem>
13031308
{objectOptions.map((o) => (
1304-
<SelectItem key={o} value={o}>{o}</SelectItem>
1309+
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
13051310
))}
13061311
</SelectContent>
13071312
</Select>

‎ui/apps/console/src/services/approvalsApi.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@
88
* API, or a dev console pointed at a remote backend) where the SameSite
99
* cookie never flows — every other console call already sends the Bearer.
1010
*
11+
* Every call goes through the console's authenticated fetch
12+
* (`createAuthenticatedFetch`), the transport the request page's data source
13+
* (`approvalRequestsDataSource.ts`) already uses. Besides the Bearer it stamps
14+
* `X-Tenant-ID` and the active UI language as `Accept-Language`, and the
15+
* approvals door serves `process_label`, `step_label` and `object_label` in
16+
* that language from the app's pack (`flows.<flow>.label`,
17+
* `flows.<flow>.approvals.<node_id>.label`, `objects.<object>.label`), the
18+
* stored text when the pack has no entry. A bare `fetch` carried only the
19+
* browser's own language, so a reader who switched the console to 中文 still
20+
* read the flow, the step and the object in the authoring language on the
21+
* inbox while the request page translated them (objectstack#26883).
22+
*
1123
* Mirrors the shape exposed by `@objectstack/plugin-approvals` /
1224
* `packages/rest/src/rest-server.ts`.
1325
*/
14-
import { TokenStorage } from '@object-ui/auth';
26+
import { createAuthenticatedFetch } from '@object-ui/auth';
1527
import type { ApprovalCancelReason } from '@objectstack/spec/contracts';
1628

1729
const SERVER_URL = (import.meta.env.VITE_SERVER_URL || '').replace(/\/$/, '');
@@ -202,12 +214,10 @@ export interface ApprovalActionRow {
202214
}
203215

204216
async function call<T>(path: string, init?: RequestInit): Promise<T> {
205-
const token = TokenStorage.get();
206-
const res = await fetch(`${API_BASE}${path}`, {
217+
const res = await createAuthenticatedFetch()(`${API_BASE}${path}`, {
207218
credentials: 'include',
208219
headers: {
209220
'Content-Type': 'application/json',
210-
...(token ? { Authorization: `Bearer ${token}` } : {}),
211221
...(init?.headers || {}),
212222
},
213223
...init,

0 commit comments

Comments
 (0)