@multiplatform.one/backoffice 7.2.2 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/config/BackofficeProvider.cjs +4 -10
- package/dist/cjs/config/BackofficeProvider.native.js +9 -29
- package/dist/cjs/config/BackofficeProvider.native.js.map +1 -1
- package/dist/cjs/desk/Desk.cjs +187 -0
- package/dist/cjs/desk/Desk.native.js +214 -0
- package/dist/cjs/desk/Desk.native.js.map +1 -0
- package/dist/cjs/desk/DeskSurface.cjs +123 -0
- package/dist/cjs/desk/DeskSurface.native.js +130 -0
- package/dist/cjs/desk/DeskSurface.native.js.map +1 -0
- package/dist/cjs/desk/resolveDeskPath.cjs +118 -0
- package/dist/cjs/desk/resolveDeskPath.native.js +123 -0
- package/dist/cjs/desk/resolveDeskPath.native.js.map +1 -0
- package/dist/cjs/desk/scope.cjs +54 -0
- package/dist/cjs/desk/scope.native.js +61 -0
- package/dist/cjs/desk/scope.native.js.map +1 -0
- package/dist/cjs/desk/target.cjs +174 -0
- package/dist/cjs/desk/target.native.js +178 -0
- package/dist/cjs/desk/target.native.js.map +1 -0
- package/dist/cjs/index.cjs +13 -0
- package/dist/cjs/index.native.js +13 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/list/DoctypeListScreen.cjs +3 -1
- package/dist/cjs/list/DoctypeListScreen.native.js +3 -1
- package/dist/cjs/list/DoctypeListScreen.native.js.map +1 -1
- package/dist/cjs/report/DoctypeReportView.cjs +3 -1
- package/dist/cjs/report/DoctypeReportView.native.js +4 -2
- package/dist/cjs/report/DoctypeReportView.native.js.map +1 -1
- package/dist/cjs/views/CalendarView.cjs +5 -0
- package/dist/cjs/views/CalendarView.native.js +5 -0
- package/dist/cjs/views/CalendarView.native.js.map +1 -1
- package/dist/cjs/views/DashboardView.cjs +5 -3
- package/dist/cjs/views/DashboardView.native.js +5 -3
- package/dist/cjs/views/DashboardView.native.js.map +1 -1
- package/dist/cjs/views/ImageView.cjs +5 -0
- package/dist/cjs/views/ImageView.native.js +5 -0
- package/dist/cjs/views/ImageView.native.js.map +1 -1
- package/dist/cjs/views/KanbanView.cjs +5 -2
- package/dist/cjs/views/KanbanView.native.js +5 -2
- package/dist/cjs/views/KanbanView.native.js.map +1 -1
- package/dist/cjs/views/TreeView.cjs +5 -0
- package/dist/cjs/views/TreeView.native.js +5 -0
- package/dist/cjs/views/TreeView.native.js.map +1 -1
- package/dist/esm/config/BackofficeProvider.mjs +5 -11
- package/dist/esm/config/BackofficeProvider.mjs.map +1 -1
- package/dist/esm/config/BackofficeProvider.native.js +10 -30
- package/dist/esm/config/BackofficeProvider.native.js.map +1 -1
- package/dist/esm/desk/Desk.mjs +162 -0
- package/dist/esm/desk/Desk.mjs.map +1 -0
- package/dist/esm/desk/Desk.native.js +186 -0
- package/dist/esm/desk/Desk.native.js.map +1 -0
- package/dist/esm/desk/DeskSurface.mjs +97 -0
- package/dist/esm/desk/DeskSurface.mjs.map +1 -0
- package/dist/esm/desk/DeskSurface.native.js +101 -0
- package/dist/esm/desk/DeskSurface.native.js.map +1 -0
- package/dist/esm/desk/resolveDeskPath.mjs +93 -0
- package/dist/esm/desk/resolveDeskPath.mjs.map +1 -0
- package/dist/esm/desk/resolveDeskPath.native.js +95 -0
- package/dist/esm/desk/resolveDeskPath.native.js.map +1 -0
- package/dist/esm/desk/scope.mjs +27 -0
- package/dist/esm/desk/scope.mjs.map +1 -0
- package/dist/esm/desk/scope.native.js +31 -0
- package/dist/esm/desk/scope.native.js.map +1 -0
- package/dist/esm/desk/target.mjs +147 -0
- package/dist/esm/desk/target.mjs.map +1 -0
- package/dist/esm/desk/target.native.js +148 -0
- package/dist/esm/desk/target.native.js.map +1 -0
- package/dist/esm/index.mjs +6 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +6 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/list/DoctypeListScreen.mjs +3 -1
- package/dist/esm/list/DoctypeListScreen.mjs.map +1 -1
- package/dist/esm/list/DoctypeListScreen.native.js +3 -1
- package/dist/esm/list/DoctypeListScreen.native.js.map +1 -1
- package/dist/esm/report/DoctypeReportView.mjs +3 -1
- package/dist/esm/report/DoctypeReportView.mjs.map +1 -1
- package/dist/esm/report/DoctypeReportView.native.js +4 -2
- package/dist/esm/report/DoctypeReportView.native.js.map +1 -1
- package/dist/esm/views/CalendarView.mjs +5 -0
- package/dist/esm/views/CalendarView.mjs.map +1 -1
- package/dist/esm/views/CalendarView.native.js +5 -0
- package/dist/esm/views/CalendarView.native.js.map +1 -1
- package/dist/esm/views/DashboardView.mjs +5 -3
- package/dist/esm/views/DashboardView.mjs.map +1 -1
- package/dist/esm/views/DashboardView.native.js +5 -3
- package/dist/esm/views/DashboardView.native.js.map +1 -1
- package/dist/esm/views/ImageView.mjs +5 -0
- package/dist/esm/views/ImageView.mjs.map +1 -1
- package/dist/esm/views/ImageView.native.js +5 -0
- package/dist/esm/views/ImageView.native.js.map +1 -1
- package/dist/esm/views/KanbanView.mjs +5 -2
- package/dist/esm/views/KanbanView.mjs.map +1 -1
- package/dist/esm/views/KanbanView.native.js +5 -2
- package/dist/esm/views/KanbanView.native.js.map +1 -1
- package/dist/esm/views/TreeView.mjs +5 -0
- package/dist/esm/views/TreeView.mjs.map +1 -1
- package/dist/esm/views/TreeView.native.js +5 -0
- package/dist/esm/views/TreeView.native.js.map +1 -1
- package/dist/jsx/config/BackofficeProvider.mjs +5 -11
- package/dist/jsx/config/BackofficeProvider.mjs.map +1 -1
- package/dist/jsx/config/BackofficeProvider.native.js +9 -29
- package/dist/jsx/config/BackofficeProvider.native.js.map +1 -1
- package/dist/jsx/desk/Desk.mjs +162 -0
- package/dist/jsx/desk/Desk.mjs.map +1 -0
- package/dist/jsx/desk/Desk.native.js +214 -0
- package/dist/jsx/desk/Desk.native.js.map +1 -0
- package/dist/jsx/desk/DeskSurface.mjs +97 -0
- package/dist/jsx/desk/DeskSurface.mjs.map +1 -0
- package/dist/jsx/desk/DeskSurface.native.js +130 -0
- package/dist/jsx/desk/DeskSurface.native.js.map +1 -0
- package/dist/jsx/desk/resolveDeskPath.mjs +93 -0
- package/dist/jsx/desk/resolveDeskPath.mjs.map +1 -0
- package/dist/jsx/desk/resolveDeskPath.native.js +123 -0
- package/dist/jsx/desk/resolveDeskPath.native.js.map +1 -0
- package/dist/jsx/desk/scope.mjs +27 -0
- package/dist/jsx/desk/scope.mjs.map +1 -0
- package/dist/jsx/desk/scope.native.js +61 -0
- package/dist/jsx/desk/scope.native.js.map +1 -0
- package/dist/jsx/desk/target.mjs +147 -0
- package/dist/jsx/desk/target.mjs.map +1 -0
- package/dist/jsx/desk/target.native.js +178 -0
- package/dist/jsx/desk/target.native.js.map +1 -0
- package/dist/jsx/index.js +6 -1
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +6 -1
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +13 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/list/DoctypeListScreen.mjs +3 -1
- package/dist/jsx/list/DoctypeListScreen.mjs.map +1 -1
- package/dist/jsx/list/DoctypeListScreen.native.js +3 -1
- package/dist/jsx/list/DoctypeListScreen.native.js.map +1 -1
- package/dist/jsx/report/DoctypeReportView.mjs +3 -1
- package/dist/jsx/report/DoctypeReportView.mjs.map +1 -1
- package/dist/jsx/report/DoctypeReportView.native.js +4 -2
- package/dist/jsx/report/DoctypeReportView.native.js.map +1 -1
- package/dist/jsx/views/CalendarView.mjs +5 -0
- package/dist/jsx/views/CalendarView.mjs.map +1 -1
- package/dist/jsx/views/CalendarView.native.js +5 -0
- package/dist/jsx/views/CalendarView.native.js.map +1 -1
- package/dist/jsx/views/DashboardView.mjs +5 -3
- package/dist/jsx/views/DashboardView.mjs.map +1 -1
- package/dist/jsx/views/DashboardView.native.js +5 -3
- package/dist/jsx/views/DashboardView.native.js.map +1 -1
- package/dist/jsx/views/ImageView.mjs +5 -0
- package/dist/jsx/views/ImageView.mjs.map +1 -1
- package/dist/jsx/views/ImageView.native.js +5 -0
- package/dist/jsx/views/ImageView.native.js.map +1 -1
- package/dist/jsx/views/KanbanView.mjs +5 -2
- package/dist/jsx/views/KanbanView.mjs.map +1 -1
- package/dist/jsx/views/KanbanView.native.js +5 -2
- package/dist/jsx/views/KanbanView.native.js.map +1 -1
- package/dist/jsx/views/TreeView.mjs +5 -0
- package/dist/jsx/views/TreeView.mjs.map +1 -1
- package/dist/jsx/views/TreeView.native.js +5 -0
- package/dist/jsx/views/TreeView.native.js.map +1 -1
- package/package.json +12 -12
- package/src/config/BackofficeProvider.tsx +11 -18
- package/src/desk/Desk.spec.tsx +287 -0
- package/src/desk/Desk.tsx +308 -0
- package/src/desk/DeskSurface.tsx +132 -0
- package/src/desk/doctrine.spec.ts +48 -0
- package/src/desk/resolveDeskPath.spec.ts +174 -0
- package/src/desk/resolveDeskPath.ts +111 -0
- package/src/desk/scope.tsx +52 -0
- package/src/desk/target.ts +169 -0
- package/src/index.ts +17 -0
- package/src/list/DoctypeListScreen.tsx +4 -1
- package/src/report/DoctypeReportView.tsx +6 -2
- package/src/views/CalendarView.tsx +3 -0
- package/src/views/DashboardView.tsx +5 -3
- package/src/views/ImageView.tsx +3 -0
- package/src/views/KanbanView.tsx +5 -1
- package/src/views/TreeView.tsx +3 -0
- package/types/config/BackofficeProvider.d.ts +6 -4
- package/types/config/BackofficeProvider.d.ts.map +1 -1
- package/types/desk/Desk.d.ts +91 -0
- package/types/desk/Desk.d.ts.map +1 -0
- package/types/desk/DeskSurface.d.ts +24 -0
- package/types/desk/DeskSurface.d.ts.map +1 -0
- package/types/desk/resolveDeskPath.d.ts +60 -0
- package/types/desk/resolveDeskPath.d.ts.map +1 -0
- package/types/desk/scope.d.ts +31 -0
- package/types/desk/scope.d.ts.map +1 -0
- package/types/desk/target.d.ts +57 -0
- package/types/desk/target.d.ts.map +1 -0
- package/types/index.d.ts +10 -0
- package/types/index.d.ts.map +1 -1
- package/types/list/DoctypeListScreen.d.ts.map +1 -1
- package/types/report/DoctypeReportView.d.ts.map +1 -1
- package/types/views/CalendarView.d.ts.map +1 -1
- package/types/views/DashboardView.d.ts.map +1 -1
- package/types/views/ImageView.d.ts.map +1 -1
- package/types/views/KanbanView.d.ts.map +1 -1
- package/types/views/TreeView.d.ts.map +1 -1
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
* It composes @multiplatform.one/frappe-ui's FrappeProvider internally (the
|
|
6
6
|
* `frappe` prop), publishes the navigation seam + capability flags + slots +
|
|
7
7
|
* event bus through BackofficeConfigContext (consumed by every backoffice
|
|
8
|
-
* surface), registers the package's "backoffice" i18n namespace, and
|
|
9
|
-
* host field
|
|
8
|
+
* surface), registers the package's "backoffice" i18n namespace, and scopes
|
|
9
|
+
* host field overrides to this subtree via FieldOverridesProvider (never the
|
|
10
|
+
* module-global registry — GL-4).
|
|
10
11
|
*
|
|
11
12
|
* Surfaces render without a provider too (stories/tests): the context
|
|
12
13
|
* default is a fully-enabled config with an inert navigator.
|
|
13
14
|
*/
|
|
14
15
|
import {
|
|
16
|
+
FieldOverridesProvider,
|
|
15
17
|
FrappeProvider,
|
|
16
|
-
registerField,
|
|
17
18
|
type FieldRenderProps,
|
|
18
19
|
type FrappeProviderProps,
|
|
19
20
|
} from "@multiplatform.one/frappe-ui";
|
|
@@ -155,8 +156,9 @@ export interface BackofficeProviderProps {
|
|
|
155
156
|
/** Restrict which doctypes are reachable (awesomebar, lists, links). */
|
|
156
157
|
doctypes?: BackofficeDoctypeScope;
|
|
157
158
|
/**
|
|
158
|
-
* Field-renderer overrides
|
|
159
|
-
*
|
|
159
|
+
* Field-renderer overrides (fieldtype → component), e.g.
|
|
160
|
+
* `{ Rating: MyRating }` — context-scoped to this provider, so two
|
|
161
|
+
* providers with different overrides coexist on one page.
|
|
160
162
|
*/
|
|
161
163
|
fields?: Record<string, ComponentType<FieldRenderProps>>;
|
|
162
164
|
/** Host-injected custom actions (form/list toolbars). */
|
|
@@ -271,18 +273,6 @@ export function BackofficeProvider({
|
|
|
271
273
|
return true;
|
|
272
274
|
});
|
|
273
275
|
|
|
274
|
-
// Host field-registry overrides — applied during render (frappe-ui's
|
|
275
|
-
// registry is module-global) so first-pass field rendering sees them.
|
|
276
|
-
const appliedFields = useRef<Record<string, ComponentType<FieldRenderProps>>>({});
|
|
277
|
-
if (fields) {
|
|
278
|
-
for (const [fieldtype, component] of Object.entries(fields)) {
|
|
279
|
-
if (appliedFields.current[fieldtype] !== component) {
|
|
280
|
-
registerField(fieldtype, component);
|
|
281
|
-
appliedFields.current[fieldtype] = component;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
276
|
// Keep the handler on a ref so the wrapped navigator's identity survives
|
|
287
277
|
// inline `onEvent` props.
|
|
288
278
|
const onEventRef = useRef<BackofficeEventHandler | undefined>(onEvent);
|
|
@@ -373,7 +363,10 @@ export function BackofficeProvider({
|
|
|
373
363
|
socketOptions={frappe.socketOptions}
|
|
374
364
|
fixtures={frappe.fixtures}
|
|
375
365
|
>
|
|
376
|
-
{
|
|
366
|
+
{/* Context-scoped field overrides (GL-4): per-provider, never the
|
|
367
|
+
module-global registry — two providers with different `fields` on
|
|
368
|
+
one page must not clobber each other. */}
|
|
369
|
+
<FieldOverridesProvider fields={fields}>{children}</FieldOverridesProvider>
|
|
377
370
|
</FrappeProvider>
|
|
378
371
|
</BackofficeConfigContext.Provider>
|
|
379
372
|
);
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Desk /> assembly specs (MPO-14):
|
|
3
|
+
* - end-to-end render over a mocked (fixture) doctype, both chrome modes
|
|
4
|
+
* - navigator-driven dispatch through the resolver + slug index
|
|
5
|
+
* - surface override registry (GL-6): per-kind and per-doctype
|
|
6
|
+
* - two Desks on one page: field-registry (GL-4) and scope (GL-5) isolation
|
|
7
|
+
*/
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
10
|
+
import { getFieldComponent, useFieldResolver } from "@multiplatform.one/frappe-ui";
|
|
11
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
12
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
13
|
+
import { describe, expect, it, vi } from "vitest";
|
|
14
|
+
import { useBackofficeNavigator, useBackofficePaths } from "../config/BackofficeProvider";
|
|
15
|
+
import { createMemoryNavigator } from "../navigation/navigator";
|
|
16
|
+
import { Desk } from "./Desk";
|
|
17
|
+
import type { DeskSurfaceProps } from "./DeskSurface";
|
|
18
|
+
import { useDeskScope } from "./scope";
|
|
19
|
+
|
|
20
|
+
const MOCK_HOST = "https://desk-assembly.mock.test";
|
|
21
|
+
|
|
22
|
+
const makeFixtures = () =>
|
|
23
|
+
new InMemoryFixtureProvider({
|
|
24
|
+
Workspace: [
|
|
25
|
+
{
|
|
26
|
+
name: "Tools",
|
|
27
|
+
doctype: "Workspace",
|
|
28
|
+
title: "Tools",
|
|
29
|
+
icon: "tool",
|
|
30
|
+
parent_page: "",
|
|
31
|
+
public: 1,
|
|
32
|
+
for_user: "",
|
|
33
|
+
sequence_id: 1,
|
|
34
|
+
module: "Automation",
|
|
35
|
+
is_hidden: 0,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
DocType: [{ name: "ToDo", doctype: "DocType" }],
|
|
39
|
+
"Notification Log": [],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const frappe = () => ({ baseURL: MOCK_HOST, fixtures: makeFixtures() });
|
|
43
|
+
|
|
44
|
+
function SurfaceProbe({ target }: DeskSurfaceProps) {
|
|
45
|
+
return <div data-testid={`probe-${target.kind}`}>{JSON.stringify(target)}</div>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("<Desk /> chrome and dispatch", () => {
|
|
49
|
+
it("renders the full desk with no required props (DK-1)", async () => {
|
|
50
|
+
renderWithProviders(<Desk frappe={frappe()} />);
|
|
51
|
+
// Full chrome mounts...
|
|
52
|
+
expect(screen.getByTestId("backoffice-shell")).toBeTruthy();
|
|
53
|
+
expect(screen.getByTestId("backoffice-awesomebar")).toBeTruthy();
|
|
54
|
+
// ...and home lands on the default workspace (fixture-backed redirect).
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(screen.getAllByText("Tools").length).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("a doctype pin renders that doctype's surface in content chrome, host URL untouched", () => {
|
|
61
|
+
const onTargetChange = vi.fn();
|
|
62
|
+
renderWithProviders(
|
|
63
|
+
<Desk
|
|
64
|
+
frappe={frappe()}
|
|
65
|
+
doctype="Sales Order"
|
|
66
|
+
surfaces={{ list: SurfaceProbe }}
|
|
67
|
+
onTargetChange={onTargetChange}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
// content chrome: no banner, no rail
|
|
71
|
+
expect(screen.queryByTestId("backoffice-shell")).toBeNull();
|
|
72
|
+
// the pin dispatched the list surface for the proper-cased doctype
|
|
73
|
+
expect(screen.getByTestId("probe-list").textContent).toContain('"Sales Order"');
|
|
74
|
+
expect(onTargetChange).toHaveBeenCalledWith({ kind: "list", doctype: "Sales Order" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("doctype + name renders the form surface; chrome='full' overrides the pin default", () => {
|
|
78
|
+
renderWithProviders(
|
|
79
|
+
<Desk
|
|
80
|
+
frappe={frappe()}
|
|
81
|
+
doctype="Sales Order"
|
|
82
|
+
name="SO-0001"
|
|
83
|
+
chrome="full"
|
|
84
|
+
surfaces={{ form: SurfaceProbe }}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
expect(screen.getByTestId("backoffice-shell")).toBeTruthy();
|
|
88
|
+
const probe = screen.getByTestId("probe-form");
|
|
89
|
+
expect(probe.textContent).toContain('"Sales Order"');
|
|
90
|
+
expect(probe.textContent).toContain('"SO-0001"');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("workspace and report pins dispatch their surfaces", () => {
|
|
94
|
+
renderWithProviders(
|
|
95
|
+
<Desk frappe={frappe()} workspace="Accounting" surfaces={{ workspace: SurfaceProbe }} />,
|
|
96
|
+
);
|
|
97
|
+
expect(screen.getByTestId("probe-workspace").textContent).toContain('"Accounting"');
|
|
98
|
+
|
|
99
|
+
renderWithProviders(
|
|
100
|
+
<Desk frappe={frappe()} report="Accounts Receivable" surfaces={{ report: SurfaceProbe }} />,
|
|
101
|
+
);
|
|
102
|
+
expect(screen.getByTestId("probe-report").textContent).toContain('"Accounts Receivable"');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("resolves navigator-driven paths through the slug index (routed mode)", async () => {
|
|
106
|
+
const navigation = createMemoryNavigator("/backoffice/todo");
|
|
107
|
+
renderWithProviders(
|
|
108
|
+
<Desk frappe={frappe()} navigation={navigation} surfaces={{ list: SurfaceProbe }} />,
|
|
109
|
+
);
|
|
110
|
+
// "todo" is ambiguous until the fixture index resolves it to ToDo.
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(screen.getByTestId("probe-list").textContent).toContain('"ToDo"');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("re-resolves when the navigator moves: list → form → back (deep-link round-trip)", async () => {
|
|
117
|
+
const navigation = createMemoryNavigator("/backoffice/todo");
|
|
118
|
+
renderWithProviders(
|
|
119
|
+
<Desk
|
|
120
|
+
frappe={frappe()}
|
|
121
|
+
navigation={navigation}
|
|
122
|
+
surfaces={{ list: SurfaceProbe, form: SurfaceProbe, "not-found": SurfaceProbe }}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
await waitFor(() => expect(screen.getByTestId("probe-list")).toBeTruthy());
|
|
126
|
+
|
|
127
|
+
navigation.navigate("/backoffice/todo/T-0001");
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(screen.getByTestId("probe-form").textContent).toContain('"T-0001"');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
navigation.back();
|
|
133
|
+
await waitFor(() => expect(screen.getByTestId("probe-list")).toBeTruthy());
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("onNavigate intercepts desk-issued navigation and cancels it (the drawer seam)", async () => {
|
|
137
|
+
// The override navigates the way every surface does: through the
|
|
138
|
+
// provider's navigator — the seam onNavigate wraps.
|
|
139
|
+
function ListWithRowPress() {
|
|
140
|
+
const navigator = useBackofficeNavigator();
|
|
141
|
+
const paths = useBackofficePaths();
|
|
142
|
+
return (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
data-testid="open-doc"
|
|
146
|
+
onClick={() => navigator.navigate(paths.form("ToDo", "T-0001"))}
|
|
147
|
+
>
|
|
148
|
+
open
|
|
149
|
+
</button>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const seen: string[] = [];
|
|
153
|
+
const navigation = createMemoryNavigator("/backoffice/todo");
|
|
154
|
+
renderWithProviders(
|
|
155
|
+
<Desk
|
|
156
|
+
frappe={frappe()}
|
|
157
|
+
navigation={navigation}
|
|
158
|
+
surfaces={{ list: ListWithRowPress, form: SurfaceProbe }}
|
|
159
|
+
onNavigate={(target) => {
|
|
160
|
+
seen.push(target.kind);
|
|
161
|
+
if (target.kind === "form") return false;
|
|
162
|
+
}}
|
|
163
|
+
/>,
|
|
164
|
+
);
|
|
165
|
+
await waitFor(() => expect(screen.getByTestId("open-doc")).toBeTruthy());
|
|
166
|
+
|
|
167
|
+
fireEvent.click(screen.getByTestId("open-doc"));
|
|
168
|
+
// cancelled: the host saw the target, the desk never left the list
|
|
169
|
+
expect(seen).toContain("form");
|
|
170
|
+
expect(navigation.getLocation().path).toBe("/backoffice/todo");
|
|
171
|
+
expect(screen.queryByTestId("probe-form")).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("per-doctype surface overrides beat per-kind, and miss falls through", () => {
|
|
175
|
+
function DoctypeProbe({ target }: DeskSurfaceProps) {
|
|
176
|
+
return <div data-testid="doctype-probe">{JSON.stringify(target)}</div>;
|
|
177
|
+
}
|
|
178
|
+
renderWithProviders(
|
|
179
|
+
<Desk
|
|
180
|
+
frappe={frappe()}
|
|
181
|
+
doctype="Sales Order"
|
|
182
|
+
surfaces={{ list: { "Sales Order": DoctypeProbe } }}
|
|
183
|
+
/>,
|
|
184
|
+
);
|
|
185
|
+
expect(screen.getByTestId("doctype-probe")).toBeTruthy();
|
|
186
|
+
|
|
187
|
+
// A record keyed by a different doctype misses → built-in list mounts
|
|
188
|
+
// (its loading state), not the probe.
|
|
189
|
+
renderWithProviders(
|
|
190
|
+
<Desk
|
|
191
|
+
frappe={frappe()}
|
|
192
|
+
doctype="ToDo"
|
|
193
|
+
surfaces={{ list: { "Sales Order": DoctypeProbe } }}
|
|
194
|
+
/>,
|
|
195
|
+
);
|
|
196
|
+
expect(screen.queryAllByTestId("doctype-probe")).toHaveLength(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("basePath rebinds resolution (mounted at /admin)", async () => {
|
|
200
|
+
const navigation = createMemoryNavigator("/admin/todo");
|
|
201
|
+
renderWithProviders(
|
|
202
|
+
<Desk
|
|
203
|
+
frappe={frappe()}
|
|
204
|
+
navigation={navigation}
|
|
205
|
+
basePath="/admin"
|
|
206
|
+
surfaces={{ list: SurfaceProbe }}
|
|
207
|
+
/>,
|
|
208
|
+
);
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
expect(screen.getByTestId("probe-list").textContent).toContain('"ToDo"');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("two Desks on one page (DK-7)", () => {
|
|
216
|
+
function FieldProbe({ testId }: { testId: string }) {
|
|
217
|
+
const resolve = useFieldResolver();
|
|
218
|
+
const Field = resolve("Data");
|
|
219
|
+
if (!Field) return null;
|
|
220
|
+
return (
|
|
221
|
+
<div data-testid={testId}>
|
|
222
|
+
<Field value="" onChange={() => {}} label="probe" />
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
it("field overrides are per-Desk and never touch the module registry (GL-4)", () => {
|
|
228
|
+
const globalData = getFieldComponent("Data");
|
|
229
|
+
const CustomA = () => <div data-testid="custom-a">A</div>;
|
|
230
|
+
const CustomB = () => <div data-testid="custom-b">B</div>;
|
|
231
|
+
const probeA = () => <FieldProbe testId="desk-a-field" />;
|
|
232
|
+
const probeB = () => <FieldProbe testId="desk-b-field" />;
|
|
233
|
+
|
|
234
|
+
renderWithProviders(
|
|
235
|
+
<>
|
|
236
|
+
<Desk
|
|
237
|
+
frappe={frappe()}
|
|
238
|
+
doctype="ToDo"
|
|
239
|
+
fields={{ Data: CustomA }}
|
|
240
|
+
surfaces={{ list: probeA }}
|
|
241
|
+
/>
|
|
242
|
+
<Desk
|
|
243
|
+
frappe={frappe()}
|
|
244
|
+
doctype="ToDo"
|
|
245
|
+
fields={{ Data: CustomB }}
|
|
246
|
+
surfaces={{ list: probeB }}
|
|
247
|
+
/>
|
|
248
|
+
</>,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Each Desk resolves its own override…
|
|
252
|
+
expect(
|
|
253
|
+
screen.getByTestId("desk-a-field").querySelector('[data-testid="custom-a"]'),
|
|
254
|
+
).toBeTruthy();
|
|
255
|
+
expect(
|
|
256
|
+
screen.getByTestId("desk-b-field").querySelector('[data-testid="custom-b"]'),
|
|
257
|
+
).toBeTruthy();
|
|
258
|
+
expect(screen.getByTestId("desk-a-field").querySelector('[data-testid="custom-b"]')).toBeNull();
|
|
259
|
+
// …and the module-global default table is untouched.
|
|
260
|
+
expect(getFieldComponent("Data")).toBe(globalData);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("scope is per-Desk context (GL-5)", () => {
|
|
264
|
+
function ScopeProbe({ testId }: { testId: string }) {
|
|
265
|
+
const scope = useDeskScope();
|
|
266
|
+
return <div data-testid={testId}>{JSON.stringify(scope)}</div>;
|
|
267
|
+
}
|
|
268
|
+
renderWithProviders(
|
|
269
|
+
<>
|
|
270
|
+
<Desk
|
|
271
|
+
frappe={frappe()}
|
|
272
|
+
doctype="ToDo"
|
|
273
|
+
scope={[["company", "=", "A"]]}
|
|
274
|
+
surfaces={{ list: () => <ScopeProbe testId="scope-a" /> }}
|
|
275
|
+
/>
|
|
276
|
+
<Desk
|
|
277
|
+
frappe={frappe()}
|
|
278
|
+
doctype="ToDo"
|
|
279
|
+
scope={[["company", "=", "B"]]}
|
|
280
|
+
surfaces={{ list: () => <ScopeProbe testId="scope-b" /> }}
|
|
281
|
+
/>
|
|
282
|
+
</>,
|
|
283
|
+
);
|
|
284
|
+
expect(screen.getByTestId("scope-a").textContent).toBe('[["company","=","A"]]');
|
|
285
|
+
expect(screen.getByTestId("scope-b").textContent).toBe('[["company","=","B"]]');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Desk /> — the whole desk as one importable component (MPO-14 / gap GL-2,
|
|
3
|
+
* spec 09). One required prop, and that prop is nothing: `<Desk />` renders
|
|
4
|
+
* the full desk; everything else is a narrowing.
|
|
5
|
+
*
|
|
6
|
+
* Desk is assembly, not implementation: prop normalization into
|
|
7
|
+
* BackofficeProvider, a navigator pick (host adapter or an internal
|
|
8
|
+
* MemoryNavigator seeded from the pins), the path→surface resolution
|
|
9
|
+
* (useDeskTarget), and the chrome pick. No fetch, no filter construction,
|
|
10
|
+
* no field mapping, no visual values — the §9.2 doctrine greps over this
|
|
11
|
+
* directory ship as a spec test (doctrine.spec.ts).
|
|
12
|
+
*/
|
|
13
|
+
import { YStack } from "@multiplatform.one/components";
|
|
14
|
+
import { useFrappeConfig, type FieldRenderProps } from "@multiplatform.one/frappe-ui";
|
|
15
|
+
import type { ComponentType } from "react";
|
|
16
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
17
|
+
import {
|
|
18
|
+
BackofficeProvider,
|
|
19
|
+
type BackofficeActions,
|
|
20
|
+
type BackofficeAppearance,
|
|
21
|
+
type BackofficeFrappeConfig,
|
|
22
|
+
type BackofficeIntegrations,
|
|
23
|
+
} from "../config/BackofficeProvider";
|
|
24
|
+
import type { BackofficeEventHandler } from "../config/events";
|
|
25
|
+
import type { BackofficeCapabilities, BackofficeDoctypeScope } from "../config/capabilities";
|
|
26
|
+
import type { BackofficeSlots } from "../config/slots";
|
|
27
|
+
import { createMemoryNavigator, type BackofficeNavigator } from "../navigation/navigator";
|
|
28
|
+
import { BACKOFFICE_BASE, createBackofficePaths, type BackofficeView } from "../navigation/paths";
|
|
29
|
+
import type { FrappeFilters } from "../shared/types";
|
|
30
|
+
import { BackofficeShell } from "../shell/BackofficeShell";
|
|
31
|
+
import { DeskSurface, DeskSurfaceLoading, type DeskSurfaceOverrides } from "./DeskSurface";
|
|
32
|
+
import { DeskScopeProvider } from "./scope";
|
|
33
|
+
import {
|
|
34
|
+
guessDeskTarget,
|
|
35
|
+
hasDeskPin,
|
|
36
|
+
useDeskTarget,
|
|
37
|
+
type DeskPins,
|
|
38
|
+
type DeskTarget,
|
|
39
|
+
} from "./target";
|
|
40
|
+
|
|
41
|
+
export type DeskChrome = "full" | "bare" | "content";
|
|
42
|
+
|
|
43
|
+
export interface DeskProps {
|
|
44
|
+
// ─── connection ────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Frappe connection. Omit to inherit an ambient FrappeProvider (or
|
|
47
|
+
* BackofficeProvider). IGNORED when an ambient provider exists — Desk
|
|
48
|
+
* warns once, because the SyncModule is cached per baseURL and a second
|
|
49
|
+
* config for the same server silently loses to the first.
|
|
50
|
+
*/
|
|
51
|
+
frappe?: BackofficeFrappeConfig;
|
|
52
|
+
|
|
53
|
+
// ─── what it shows (the pins) ──────────────────────────────────────────
|
|
54
|
+
/** Pin the desk to one doctype. Sets the initial target AND the scope. */
|
|
55
|
+
doctype?: string;
|
|
56
|
+
/** With `doctype`: a document name, or "new". Renders the form. */
|
|
57
|
+
name?: string;
|
|
58
|
+
/** With `doctype`: which view. Default "list". */
|
|
59
|
+
view?: BackofficeView;
|
|
60
|
+
/** Pin the desk to one workspace page. */
|
|
61
|
+
workspace?: string;
|
|
62
|
+
/** Pin the desk to one Query/Script Report. */
|
|
63
|
+
report?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Explicit surface address relative to `basePath` (e.g.
|
|
66
|
+
* "/sales-order/SO-0001"). Wins over every pin — the escape hatch for a
|
|
67
|
+
* host with its own address scheme.
|
|
68
|
+
*/
|
|
69
|
+
path?: string;
|
|
70
|
+
|
|
71
|
+
// ─── scoping ───────────────────────────────────────────────────────────
|
|
72
|
+
/** Seed the user-visible filter channel; the user can change or clear. */
|
|
73
|
+
filters?: FrappeFilters;
|
|
74
|
+
/**
|
|
75
|
+
* ALWAYS ANDed into every doctype-homed query, invisible to the filter
|
|
76
|
+
* chrome, not removable — the tenant seam (DeskScopeProvider).
|
|
77
|
+
*/
|
|
78
|
+
scope?: FrappeFilters;
|
|
79
|
+
/** Doctype reachability. A `doctype` pin defaults this to that doctype. */
|
|
80
|
+
doctypes?: BackofficeDoctypeScope;
|
|
81
|
+
|
|
82
|
+
// ─── which surfaces are enabled ────────────────────────────────────────
|
|
83
|
+
/** Feature flags, everything defaults on (config/capabilities.ts). */
|
|
84
|
+
capabilities?: BackofficeCapabilities;
|
|
85
|
+
/** View-switcher allowlist, intersected with capabilities.views.allow. */
|
|
86
|
+
views?: readonly BackofficeView[];
|
|
87
|
+
/** Force every form read-only regardless of permissions. */
|
|
88
|
+
readOnly?: boolean;
|
|
89
|
+
|
|
90
|
+
// ─── chrome ────────────────────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* "full" banner + workspace rail; "bare" banner only; "content" no
|
|
93
|
+
* chrome. Default: "content" when any pin is set, "full" otherwise.
|
|
94
|
+
*/
|
|
95
|
+
chrome?: DeskChrome;
|
|
96
|
+
/** Sugar for chrome="content". */
|
|
97
|
+
embedded?: boolean;
|
|
98
|
+
/** Render-prop chrome replacements (config/slots.ts). */
|
|
99
|
+
slots?: BackofficeSlots;
|
|
100
|
+
/** Color-scheme adapter for the user menu's "Toggle Theme". */
|
|
101
|
+
appearance?: BackofficeAppearance;
|
|
102
|
+
/** Router-integration hooks (navigation blocker, …). */
|
|
103
|
+
integrations?: BackofficeIntegrations;
|
|
104
|
+
|
|
105
|
+
// ─── routing ───────────────────────────────────────────────────────────
|
|
106
|
+
/**
|
|
107
|
+
* Host router adapter. Supply it and Desk owns URLs; omit it and Desk
|
|
108
|
+
* navigates internally over a MemoryNavigator seeded from the pins.
|
|
109
|
+
*/
|
|
110
|
+
navigation?: BackofficeNavigator;
|
|
111
|
+
/** URL prefix Desk is mounted under. Default "/backoffice". */
|
|
112
|
+
basePath?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Intercept push/replace navigations before they happen; return false to
|
|
115
|
+
* cancel (host renders the target itself, e.g. in a drawer). The target
|
|
116
|
+
* is resolved best-effort: an ambiguous slug degrades to a title-cased
|
|
117
|
+
* doctype guess, the same degrade the slug index applies offline.
|
|
118
|
+
*/
|
|
119
|
+
onNavigate?: (target: DeskTarget, path: string) => boolean | void;
|
|
120
|
+
/** Fires whenever the resolved surface changes (host breadcrumbs/titles). */
|
|
121
|
+
onTargetChange?: (target: DeskTarget) => void;
|
|
122
|
+
|
|
123
|
+
// ─── extension ─────────────────────────────────────────────────────────
|
|
124
|
+
/** Replace a whole surface, per kind or per doctype under a kind. */
|
|
125
|
+
surfaces?: DeskSurfaceOverrides;
|
|
126
|
+
/** Replace a field control by fieldtype — context-scoped per Desk. */
|
|
127
|
+
fields?: Record<string, ComponentType<FieldRenderProps>>;
|
|
128
|
+
/** Host actions on form and list toolbars, optionally per-doctype. */
|
|
129
|
+
actions?: BackofficeActions;
|
|
130
|
+
/** Every notable desk action lands here (config/events.ts). */
|
|
131
|
+
onEvent?: BackofficeEventHandler;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Pins → the internal navigator's seed path. */
|
|
135
|
+
function pinnedPath(pins: DeskPins, basePath: string): string {
|
|
136
|
+
const paths = createBackofficePaths(basePath);
|
|
137
|
+
if (pins.path) {
|
|
138
|
+
const prefix = basePath === "/" ? "" : basePath.replace(/\/+$/, "");
|
|
139
|
+
return `${prefix}${pins.path}`;
|
|
140
|
+
}
|
|
141
|
+
if (pins.doctype) {
|
|
142
|
+
if (pins.name === "new") return paths.newDoc(pins.doctype);
|
|
143
|
+
if (pins.name) return paths.form(pins.doctype, pins.name);
|
|
144
|
+
if (pins.view && pins.view !== "list") return paths.view(pins.doctype, pins.view);
|
|
145
|
+
return paths.list(pins.doctype);
|
|
146
|
+
}
|
|
147
|
+
if (pins.workspace) return paths.workspace(pins.workspace);
|
|
148
|
+
if (pins.report) return paths.queryReport(pins.report);
|
|
149
|
+
return paths.home();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function Desk(props: DeskProps) {
|
|
153
|
+
const {
|
|
154
|
+
doctype,
|
|
155
|
+
name,
|
|
156
|
+
view,
|
|
157
|
+
workspace,
|
|
158
|
+
report,
|
|
159
|
+
path,
|
|
160
|
+
basePath = BACKOFFICE_BASE,
|
|
161
|
+
chrome: chromeProp,
|
|
162
|
+
embedded,
|
|
163
|
+
onNavigate,
|
|
164
|
+
} = props;
|
|
165
|
+
|
|
166
|
+
const pins = useMemo<DeskPins>(
|
|
167
|
+
() => ({ doctype, name, view, workspace, report, path }),
|
|
168
|
+
[doctype, name, view, workspace, report, path],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// ── connection: ambient provider wins over the prop, warned once ──────
|
|
172
|
+
const ambient = useFrappeConfig();
|
|
173
|
+
const warned = useRef(false);
|
|
174
|
+
if (ambient && props.frappe && !warned.current) {
|
|
175
|
+
warned.current = true;
|
|
176
|
+
console.warn(
|
|
177
|
+
"[backoffice] <Desk frappe> is ignored: an ambient FrappeProvider exists and the SyncModule is cached per baseURL.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const frappe = useMemo<BackofficeFrappeConfig>(() => {
|
|
181
|
+
if (ambient) {
|
|
182
|
+
return { baseURL: ambient.baseURL, auth: ambient.auth, fixtures: ambient.fixtures };
|
|
183
|
+
}
|
|
184
|
+
return props.frappe ?? { baseURL: "" };
|
|
185
|
+
}, [ambient, props.frappe]);
|
|
186
|
+
|
|
187
|
+
// ── navigation: host adapter, else internal memory (seeded once) ──────
|
|
188
|
+
const memoryNavigator = useMemo(
|
|
189
|
+
() => (props.navigation ? undefined : createMemoryNavigator(pinnedPath(pins, basePath))),
|
|
190
|
+
// The seed is the ENTRY; later pin changes re-enter through useDeskTarget.
|
|
191
|
+
// oxlint-disable-next-line exhaustive-deps
|
|
192
|
+
[props.navigation ? "host" : "memory"],
|
|
193
|
+
);
|
|
194
|
+
const baseNavigation = props.navigation ?? memoryNavigator!;
|
|
195
|
+
|
|
196
|
+
const onNavigateRef = useRef(onNavigate);
|
|
197
|
+
onNavigateRef.current = onNavigate;
|
|
198
|
+
const navigation = useMemo<BackofficeNavigator>(() => {
|
|
199
|
+
const intercept = (path: string): boolean =>
|
|
200
|
+
onNavigateRef.current?.(guessDeskTarget(path, basePath), path) !== false;
|
|
201
|
+
return {
|
|
202
|
+
...baseNavigation,
|
|
203
|
+
navigate: (path) => {
|
|
204
|
+
if (intercept(path)) baseNavigation.navigate(path);
|
|
205
|
+
},
|
|
206
|
+
replace: (path) => {
|
|
207
|
+
if (intercept(path)) baseNavigation.replace(path);
|
|
208
|
+
},
|
|
209
|
+
back: () => baseNavigation.back(),
|
|
210
|
+
getLocation: () => baseNavigation.getLocation(),
|
|
211
|
+
subscribe: (listener) => baseNavigation.subscribe(listener),
|
|
212
|
+
...(baseNavigation.setParams
|
|
213
|
+
? {
|
|
214
|
+
setParams: (params: Record<string, string | undefined>) =>
|
|
215
|
+
baseNavigation.setParams?.(params),
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
};
|
|
219
|
+
}, [baseNavigation, basePath]);
|
|
220
|
+
|
|
221
|
+
// ── narrowings: chrome, doctype fence, bare-chrome capability merge ───
|
|
222
|
+
const chrome: DeskChrome = chromeProp ?? (embedded || hasDeskPin(pins) ? "content" : "full");
|
|
223
|
+
const doctypes = props.doctypes ?? (doctype ? { allow: [doctype] } : undefined);
|
|
224
|
+
const capabilities = useMemo<BackofficeCapabilities | undefined>(
|
|
225
|
+
() => (chrome === "bare" ? { ...props.capabilities, sidebar: false } : props.capabilities),
|
|
226
|
+
[chrome, props.capabilities],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<BackofficeProvider
|
|
231
|
+
frappe={frappe}
|
|
232
|
+
navigation={navigation}
|
|
233
|
+
basePath={basePath}
|
|
234
|
+
capabilities={capabilities}
|
|
235
|
+
doctypes={doctypes}
|
|
236
|
+
fields={props.fields}
|
|
237
|
+
actions={props.actions}
|
|
238
|
+
slots={props.slots}
|
|
239
|
+
appearance={props.appearance}
|
|
240
|
+
integrations={props.integrations}
|
|
241
|
+
onEvent={props.onEvent}
|
|
242
|
+
>
|
|
243
|
+
<DeskScopeProvider scope={props.scope}>
|
|
244
|
+
<DeskBody
|
|
245
|
+
pins={pins}
|
|
246
|
+
chrome={chrome}
|
|
247
|
+
readOnly={props.readOnly}
|
|
248
|
+
surfaces={props.surfaces}
|
|
249
|
+
initialFilters={props.filters}
|
|
250
|
+
views={props.views}
|
|
251
|
+
onTargetChange={props.onTargetChange}
|
|
252
|
+
/>
|
|
253
|
+
</DeskScopeProvider>
|
|
254
|
+
</BackofficeProvider>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface DeskBodyProps {
|
|
259
|
+
pins: DeskPins;
|
|
260
|
+
chrome: DeskChrome;
|
|
261
|
+
readOnly?: boolean;
|
|
262
|
+
surfaces?: DeskSurfaceOverrides;
|
|
263
|
+
initialFilters?: FrappeFilters;
|
|
264
|
+
views?: readonly BackofficeView[];
|
|
265
|
+
onTargetChange?: (target: DeskTarget) => void;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Inside the provider: resolve the target, pick the chrome, mount the surface. */
|
|
269
|
+
function DeskBody({
|
|
270
|
+
pins,
|
|
271
|
+
chrome,
|
|
272
|
+
readOnly,
|
|
273
|
+
surfaces,
|
|
274
|
+
initialFilters,
|
|
275
|
+
views,
|
|
276
|
+
onTargetChange,
|
|
277
|
+
}: DeskBodyProps) {
|
|
278
|
+
const target = useDeskTarget(pins);
|
|
279
|
+
|
|
280
|
+
const onTargetChangeRef = useRef(onTargetChange);
|
|
281
|
+
onTargetChangeRef.current = onTargetChange;
|
|
282
|
+
const targetKey = target ? JSON.stringify(target) : undefined;
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (target) onTargetChangeRef.current?.(target);
|
|
285
|
+
// oxlint-disable-next-line exhaustive-deps
|
|
286
|
+
}, [targetKey]);
|
|
287
|
+
|
|
288
|
+
const surface = target ? (
|
|
289
|
+
<DeskSurface
|
|
290
|
+
target={target}
|
|
291
|
+
overrides={surfaces}
|
|
292
|
+
readOnly={readOnly}
|
|
293
|
+
initialFilters={initialFilters}
|
|
294
|
+
views={views}
|
|
295
|
+
/>
|
|
296
|
+
) : (
|
|
297
|
+
<DeskSurfaceLoading />
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (chrome === "content") {
|
|
301
|
+
return (
|
|
302
|
+
<YStack flex={1} minHeight={0} minWidth={0}>
|
|
303
|
+
{surface}
|
|
304
|
+
</YStack>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return <BackofficeShell>{surface}</BackofficeShell>;
|
|
308
|
+
}
|