@multiplatform.one/frappe-ui 7.23.1 → 7.23.2
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/FrappeForm.cjs +8 -0
- package/dist/cjs/FrappeForm.native.js +9 -0
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +14 -1
- package/dist/cjs/FrappeListView.native.js +15 -1
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeNotConfigured.cjs +54 -0
- package/dist/cjs/FrappeNotConfigured.native.js +58 -0
- package/dist/cjs/FrappeNotConfigured.native.js.map +1 -0
- package/dist/cjs/FrappeReportView.cjs +1 -1
- package/dist/cjs/FrappeReportView.native.js +2 -2
- package/dist/cjs/FrappeReportView.native.js.map +1 -1
- package/dist/cjs/fields/CodeEditor/index.cjs +1 -1
- package/dist/cjs/fields/CodeEditor/index.native.js +1 -1
- package/dist/cjs/fields/CodeEditor/index.native.js.map +1 -1
- package/dist/cjs/fields/Color/index.cjs +1 -0
- package/dist/cjs/fields/Color/index.native.js +1 -0
- package/dist/cjs/fields/Color/index.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload.cjs +1 -1
- package/dist/cjs/fields/FileUpload.native.js +1 -1
- package/dist/cjs/fields/FileUpload.native.js.map +1 -1
- package/dist/cjs/fields/Heading/index.cjs +1 -1
- package/dist/cjs/fields/Heading/index.native.js +1 -1
- package/dist/cjs/fields/Heading/index.native.js.map +1 -1
- package/dist/cjs/fields/MarkdownEditor/index.cjs +2 -1
- package/dist/cjs/fields/MarkdownEditor/index.native.js +1 -1
- package/dist/cjs/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/cjs/useFrappeFacets.cjs +1 -1
- package/dist/cjs/useFrappeFacets.native.js +1 -1
- package/dist/cjs/useFrappeFacets.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +8 -0
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +9 -0
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +14 -1
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +15 -1
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/FrappeNotConfigured.mjs +29 -0
- package/dist/esm/FrappeNotConfigured.mjs.map +1 -0
- package/dist/esm/FrappeNotConfigured.native.js +30 -0
- package/dist/esm/FrappeNotConfigured.native.js.map +1 -0
- package/dist/esm/FrappeReportView.mjs +1 -1
- package/dist/esm/FrappeReportView.mjs.map +1 -1
- package/dist/esm/FrappeReportView.native.js +2 -2
- package/dist/esm/FrappeReportView.native.js.map +1 -1
- package/dist/esm/fields/CodeEditor/index.mjs +2 -2
- package/dist/esm/fields/CodeEditor/index.mjs.map +1 -1
- package/dist/esm/fields/CodeEditor/index.native.js +2 -2
- package/dist/esm/fields/CodeEditor/index.native.js.map +1 -1
- package/dist/esm/fields/Color/index.mjs +1 -0
- package/dist/esm/fields/Color/index.mjs.map +1 -1
- package/dist/esm/fields/Color/index.native.js +1 -0
- package/dist/esm/fields/Color/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload.mjs +2 -2
- package/dist/esm/fields/FileUpload.mjs.map +1 -1
- package/dist/esm/fields/FileUpload.native.js +2 -2
- package/dist/esm/fields/FileUpload.native.js.map +1 -1
- package/dist/esm/fields/Heading/index.mjs +2 -2
- package/dist/esm/fields/Heading/index.mjs.map +1 -1
- package/dist/esm/fields/Heading/index.native.js +2 -2
- package/dist/esm/fields/Heading/index.native.js.map +1 -1
- package/dist/esm/fields/MarkdownEditor/index.mjs +2 -1
- package/dist/esm/fields/MarkdownEditor/index.mjs.map +1 -1
- package/dist/esm/fields/MarkdownEditor/index.native.js +2 -2
- package/dist/esm/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/esm/useFrappeFacets.mjs +1 -1
- package/dist/esm/useFrappeFacets.mjs.map +1 -1
- package/dist/esm/useFrappeFacets.native.js +1 -1
- package/dist/esm/useFrappeFacets.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +8 -0
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +9 -0
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +14 -1
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +15 -1
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/FrappeNotConfigured.mjs +29 -0
- package/dist/jsx/FrappeNotConfigured.mjs.map +1 -0
- package/dist/jsx/FrappeNotConfigured.native.js +58 -0
- package/dist/jsx/FrappeNotConfigured.native.js.map +1 -0
- package/dist/jsx/FrappeReportView.mjs +1 -1
- package/dist/jsx/FrappeReportView.mjs.map +1 -1
- package/dist/jsx/FrappeReportView.native.js +2 -2
- package/dist/jsx/FrappeReportView.native.js.map +1 -1
- package/dist/jsx/fields/CodeEditor/index.mjs +2 -2
- package/dist/jsx/fields/CodeEditor/index.mjs.map +1 -1
- package/dist/jsx/fields/CodeEditor/index.native.js +1 -1
- package/dist/jsx/fields/CodeEditor/index.native.js.map +1 -1
- package/dist/jsx/fields/Color/index.mjs +1 -0
- package/dist/jsx/fields/Color/index.mjs.map +1 -1
- package/dist/jsx/fields/Color/index.native.js +1 -0
- package/dist/jsx/fields/Color/index.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload.mjs +2 -2
- package/dist/jsx/fields/FileUpload.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload.native.js +1 -1
- package/dist/jsx/fields/FileUpload.native.js.map +1 -1
- package/dist/jsx/fields/Heading/index.mjs +2 -2
- package/dist/jsx/fields/Heading/index.mjs.map +1 -1
- package/dist/jsx/fields/Heading/index.native.js +1 -1
- package/dist/jsx/fields/Heading/index.native.js.map +1 -1
- package/dist/jsx/fields/MarkdownEditor/index.mjs +2 -1
- package/dist/jsx/fields/MarkdownEditor/index.mjs.map +1 -1
- package/dist/jsx/fields/MarkdownEditor/index.native.js +1 -1
- package/dist/jsx/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/jsx/useFrappeFacets.mjs +1 -1
- package/dist/jsx/useFrappeFacets.mjs.map +1 -1
- package/dist/jsx/useFrappeFacets.native.js +1 -1
- package/dist/jsx/useFrappeFacets.native.js.map +1 -1
- package/package.json +16 -16
- package/src/FormSidebar.stories.tsx +2 -0
- package/src/FrappeCalendar.stories.tsx +2 -0
- package/src/FrappeChartCard.stories.tsx +2 -0
- package/src/FrappeDashboard.stories.tsx +2 -0
- package/src/FrappeForm.tsx +7 -1
- package/src/FrappeImageView.stories.tsx +2 -0
- package/src/FrappeKPICard.stories.tsx +3 -3
- package/src/FrappeKanban.stories.tsx +4 -8
- package/src/FrappeListView.stories.tsx +2 -0
- package/src/FrappeListView.tsx +14 -1
- package/src/FrappeNotConfigured.tsx +29 -0
- package/src/FrappeReportView.tsx +1 -1
- package/src/clientCache.auth.spec.tsx +94 -0
- package/src/desk/DeskFormSidebar.stories.tsx +2 -0
- package/src/desk/DeskShell.stories.tsx +12 -6
- package/src/fields/CodeEditor/index.tsx +8 -2
- package/src/fields/Color/Color.stories.tsx +2 -0
- package/src/fields/Color/index.tsx +1 -0
- package/src/fields/FileUpload.tsx +8 -2
- package/src/fields/Heading/index.tsx +8 -2
- package/src/fields/MarkdownEditor/index.native.tsx +8 -2
- package/src/fields/MarkdownEditor/index.tsx +8 -1
- package/src/fields/ReadOnly/ReadOnly.stories.tsx +2 -0
- package/src/frappeRoutes.notConfigured.spec.tsx +77 -0
- package/src/useFrappeFacets.ts +14 -7
- package/types/FrappeForm.d.ts +2 -1
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeListView.d.ts +3 -1
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/FrappeNotConfigured.d.ts +11 -0
- package/types/FrappeNotConfigured.d.ts.map +1 -0
- package/types/fields/CodeEditor/index.d.ts.map +1 -1
- package/types/fields/Color/index.d.ts.map +1 -1
- package/types/fields/FileUpload.d.ts.map +1 -1
- package/types/fields/Heading/index.d.ts.map +1 -1
- package/types/fields/MarkdownEditor/index.d.ts.map +1 -1
- package/types/fields/MarkdownEditor/index.native.d.ts.map +1 -1
- package/types/useFrappeFacets.d.ts.map +1 -1
|
@@ -29,6 +29,8 @@ import { FrappeForm } from "./FrappeForm";
|
|
|
29
29
|
import { FrappeProvider } from "./FrappeProvider";
|
|
30
30
|
import { FrappeStoryProvider, LiveToDoGate, frappeStoriesLive } from "./frappeStoryTarget";
|
|
31
31
|
|
|
32
|
+
// hex-escape: generated image fixtures are data, not paint
|
|
33
|
+
|
|
32
34
|
export default {
|
|
33
35
|
title: "FrappeUI/FormSidebar",
|
|
34
36
|
component: FormSidebar,
|
|
@@ -6,6 +6,8 @@ import { FrappeCalendar } from "./FrappeCalendar";
|
|
|
6
6
|
import { FrappeProvider } from "./FrappeProvider";
|
|
7
7
|
import { FrappeStoryProvider } from "./frappeStoryTarget";
|
|
8
8
|
|
|
9
|
+
// hex-escape: Frappe sends column, event and series colours as hex: fixture data, not paint
|
|
10
|
+
|
|
9
11
|
export default {
|
|
10
12
|
title: "FrappeUI/FrappeCalendar",
|
|
11
13
|
component: FrappeCalendar,
|
|
@@ -5,6 +5,8 @@ import { FrappeChartCard } from "./FrappeChartCard";
|
|
|
5
5
|
import { FrappeProvider } from "./FrappeProvider";
|
|
6
6
|
import { FrappeStoryProvider } from "./frappeStoryTarget";
|
|
7
7
|
|
|
8
|
+
// hex-escape: Frappe sends column, event and series colours as hex: fixture data, not paint
|
|
9
|
+
|
|
8
10
|
export default {
|
|
9
11
|
title: "FrappeUI/FrappeChartCard",
|
|
10
12
|
component: ChartCard,
|
|
@@ -5,6 +5,8 @@ import { FrappeDashboard } from "./FrappeDashboard";
|
|
|
5
5
|
import { FrappeProvider } from "./FrappeProvider";
|
|
6
6
|
import { FrappeStoryProvider } from "./frappeStoryTarget";
|
|
7
7
|
|
|
8
|
+
// hex-escape: Frappe sends column, event and series colours as hex: fixture data, not paint
|
|
9
|
+
|
|
8
10
|
export default {
|
|
9
11
|
title: "FrappeUI/FrappeDashboard",
|
|
10
12
|
component: FrappeDashboard,
|
package/src/FrappeForm.tsx
CHANGED
|
@@ -52,6 +52,7 @@ import { extractServerError } from "./formEngine/useFormEngine";
|
|
|
52
52
|
import { FrappeFormActions } from "./FrappeFormActions";
|
|
53
53
|
import { connectionFields, connectionsFromMeta, type FrappeConnection } from "./frappeConnections";
|
|
54
54
|
import { FrappeFormConnections } from "./FrappeFormConnections";
|
|
55
|
+
import { FrappeNotConfigured } from "./FrappeNotConfigured";
|
|
55
56
|
import { FrappeRowsReader, type FrappeRowsSnapshot } from "./FrappeRowsReader";
|
|
56
57
|
import {
|
|
57
58
|
frappeRouteHref,
|
|
@@ -377,12 +378,17 @@ export function mapDocToSidebarMetadata(
|
|
|
377
378
|
* `buildFormTabs` (tabs, sections, `depends_on`, `mandatory_depends_on`,
|
|
378
379
|
* `read_only_depends_on`, `collapsible_depends_on`, permlevel,
|
|
379
380
|
* `set_only_once`, `fetch_from` and docstatus), and renders each tab with
|
|
380
|
-
* `SchemaForm`. With a `route` it is the doctype's record page.
|
|
381
|
+
* `SchemaForm`. With a `route` it is the doctype's record page. With neither
|
|
382
|
+
* a bench URL nor a fixture corpus it says Frappe is not configured.
|
|
381
383
|
*/
|
|
382
384
|
export function FrappeForm<TDoc extends { name: string; doctype: string }>(
|
|
383
385
|
props: FrappeFormProps<TDoc>,
|
|
384
386
|
) {
|
|
387
|
+
const config = useFrappeConfig();
|
|
385
388
|
const { route, ...formProps } = props;
|
|
389
|
+
if (!(props.baseURL ?? config?.baseURL) && !config?.fixtures) {
|
|
390
|
+
return <FrappeNotConfigured doctype={props.doctype} page={route !== undefined} />;
|
|
391
|
+
}
|
|
386
392
|
if (route === undefined) return <FrappeFormBody<TDoc> {...formProps} />;
|
|
387
393
|
return <FrappeFormRoute<TDoc> {...formProps} route={route} />;
|
|
388
394
|
}
|
|
@@ -4,6 +4,8 @@ import { FrappeImageView } from "./FrappeImageView";
|
|
|
4
4
|
import { FrappeProvider } from "./FrappeProvider";
|
|
5
5
|
import { FrappeStoryProvider } from "./frappeStoryTarget";
|
|
6
6
|
|
|
7
|
+
// hex-escape: generated image fixtures are data, not paint
|
|
8
|
+
|
|
7
9
|
export default {
|
|
8
10
|
title: "FrappeUI/FrappeImageView",
|
|
9
11
|
component: FrappeImageView,
|
|
@@ -4,6 +4,7 @@ import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
|
4
4
|
// catalog names it does not define).
|
|
5
5
|
import { KPICard } from "@multiplatform.one/components";
|
|
6
6
|
import { action } from "@multiplatform.one/storybook";
|
|
7
|
+
import { sizeRecipeEscape } from "@multiplatform.one/theme";
|
|
7
8
|
import { CurrencyDollarIcon } from "@phosphor-icons/react";
|
|
8
9
|
import { XStack } from "tamagui";
|
|
9
10
|
import { FrappeKPICard } from "./FrappeKPICard";
|
|
@@ -81,8 +82,7 @@ function jsonResponse(body: unknown, status = 200): Response {
|
|
|
81
82
|
return jsonResponse({ docs: [todoMetaRaw(decodeURIComponent(doctype))] });
|
|
82
83
|
}
|
|
83
84
|
const metaMatch = url.pathname.match(/^\/api\/resource\/DocType\/(.+)$/);
|
|
84
|
-
if (metaMatch)
|
|
85
|
-
return jsonResponse({ data: todoMetaRaw(decodeURIComponent(metaMatch[1])) });
|
|
85
|
+
if (metaMatch) return jsonResponse({ data: todoMetaRaw(decodeURIComponent(metaMatch[1])) });
|
|
86
86
|
if (url.pathname === "/api/resource/DocType") {
|
|
87
87
|
return jsonResponse({ data: [{ name: "ToDo", module: "Desk" }] });
|
|
88
88
|
}
|
|
@@ -201,7 +201,7 @@ export const Clickable = () => (
|
|
|
201
201
|
|
|
202
202
|
export const LongTitle = () => (
|
|
203
203
|
<KPICard
|
|
204
|
-
width={220}
|
|
204
|
+
width={sizeRecipeEscape(220, "story fixture: a card narrow enough to truncate the long title")}
|
|
205
205
|
config={{
|
|
206
206
|
id: "long",
|
|
207
207
|
title:
|
|
@@ -5,6 +5,8 @@ import { FrappeKanban, type KanbanColumn } from "./FrappeKanban";
|
|
|
5
5
|
import { FrappeProvider } from "./FrappeProvider";
|
|
6
6
|
import { FrappeStoryProvider } from "./frappeStoryTarget";
|
|
7
7
|
|
|
8
|
+
// hex-escape: Frappe sends column, event and series colours as hex: fixture data, not paint
|
|
9
|
+
|
|
8
10
|
export default {
|
|
9
11
|
title: "FrappeUI/FrappeKanban",
|
|
10
12
|
component: FrappeKanban,
|
|
@@ -310,10 +312,7 @@ const bugColumns: KanbanColumn[] = [
|
|
|
310
312
|
];
|
|
311
313
|
|
|
312
314
|
export const BugTracker = () => (
|
|
313
|
-
<FrappeProvider
|
|
314
|
-
baseURL={MOCK_HOST}
|
|
315
|
-
fixtures={new InMemoryFixtureProvider({ Bug: bugFixtures })}
|
|
316
|
-
>
|
|
315
|
+
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({ Bug: bugFixtures })}>
|
|
317
316
|
<FrappeKanban<Bug>
|
|
318
317
|
doctype="Bug"
|
|
319
318
|
columns={bugColumns}
|
|
@@ -360,10 +359,7 @@ export const Loading = () => (
|
|
|
360
359
|
|
|
361
360
|
/** Zero documents, no failure — configured columns render with per-column "No items" (empty ≠ error). */
|
|
362
361
|
export const Empty = () => (
|
|
363
|
-
<FrappeProvider
|
|
364
|
-
baseURL={MOCK_HOST}
|
|
365
|
-
fixtures={new InMemoryFixtureProvider({ ToDo: [] })}
|
|
366
|
-
>
|
|
362
|
+
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({ ToDo: [] })}>
|
|
367
363
|
<FrappeKanban<ToDo>
|
|
368
364
|
doctype="ToDo"
|
|
369
365
|
columns={todoColumns}
|
|
@@ -4,6 +4,8 @@ import { FrappeListView } from "./FrappeListView";
|
|
|
4
4
|
import { FrappeProvider } from "./FrappeProvider";
|
|
5
5
|
import { FrappeStoryProvider } from "./frappeStoryTarget";
|
|
6
6
|
|
|
7
|
+
// hex-escape: generated image fixtures are data, not paint
|
|
8
|
+
|
|
7
9
|
export default {
|
|
8
10
|
title: "FrappeUI/FrappeListView",
|
|
9
11
|
component: FrappeListView,
|
package/src/FrappeListView.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { pressTargetStyle, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
|
19
19
|
import { storage } from "@multiplatform.one/store";
|
|
20
20
|
import { useFrappeConfig } from "./FrappeProvider";
|
|
21
21
|
import { FrappeList, type FrappeListProps } from "./FrappeList";
|
|
22
|
+
import { FrappeNotConfigured } from "./FrappeNotConfigured";
|
|
22
23
|
import { FrappeTable } from "./FrappeTable";
|
|
23
24
|
import { useDocRights } from "./formEngine/useDocRights";
|
|
24
25
|
import { frappeRouteHref, useFrappeRouter } from "./frappeNavigation";
|
|
@@ -170,8 +171,20 @@ async function setStoredViewMode(doctype: string, mode: ListViewMode): Promise<v
|
|
|
170
171
|
*
|
|
171
172
|
* Delegates to `FrappeList` (which delegates to generic `List`) or
|
|
172
173
|
* `FrappeTable` (which delegates to `DataTable`) based on the view mode.
|
|
174
|
+
* With neither a bench URL nor a fixture corpus it says Frappe is not
|
|
175
|
+
* configured.
|
|
173
176
|
*/
|
|
174
|
-
export function FrappeListView<TDoc extends { name: string; doctype: string }>(
|
|
177
|
+
export function FrappeListView<TDoc extends { name: string; doctype: string }>(
|
|
178
|
+
props: FrappeListViewProps<TDoc>,
|
|
179
|
+
) {
|
|
180
|
+
const config = useFrappeConfig();
|
|
181
|
+
if (!(props.baseURL ?? config?.baseURL) && !config?.fixtures) {
|
|
182
|
+
return <FrappeNotConfigured doctype={props.doctype} page={props.route !== undefined} />;
|
|
183
|
+
}
|
|
184
|
+
return <FrappeListViewBody<TDoc> {...props} />;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function FrappeListViewBody<TDoc extends { name: string; doctype: string }>({
|
|
175
188
|
doctype,
|
|
176
189
|
fields,
|
|
177
190
|
connections,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EmptyState, ListPageLayout, PageHeader } from "@multiplatform.one/components";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
export interface FrappeNotConfiguredProps {
|
|
5
|
+
doctype: string;
|
|
6
|
+
/** A routed page keeps the page template, the doctype as its title. */
|
|
7
|
+
page?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What a doctype's list or form shows when neither a bench URL nor a fixture
|
|
12
|
+
* corpus is configured (a host built with Frappe off), in place of the view.
|
|
13
|
+
*/
|
|
14
|
+
export function FrappeNotConfigured({ doctype, page = false }: FrappeNotConfiguredProps) {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const state = (
|
|
17
|
+
<EmptyState
|
|
18
|
+
testID="frappe-not-configured"
|
|
19
|
+
title={t("Frappe is not configured")}
|
|
20
|
+
description={t("{{doctype}} can't load without a Frappe bench URL.", { doctype })}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
if (!page) return state;
|
|
24
|
+
return (
|
|
25
|
+
<ListPageLayout scroll={false} header={<PageHeader title={doctype} divider={false} />}>
|
|
26
|
+
{state}
|
|
27
|
+
</ListPageLayout>
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/FrappeReportView.tsx
CHANGED
|
@@ -349,7 +349,7 @@ export interface FrappeReportConnection {
|
|
|
349
349
|
const callCache = new Map<string, CallModule>();
|
|
350
350
|
|
|
351
351
|
function getCall(connection: FrappeReportConnection): CallModule {
|
|
352
|
-
const key = `${connection.baseURL}:${connection.auth?.token ?? ""}:${connection.auth?.type ?? ""}`;
|
|
352
|
+
const key = `${connection.baseURL}:${connection.auth?.useToken ? "t" : ""}:${connection.auth?.token ?? ""}:${connection.auth?.type ?? ""}`;
|
|
353
353
|
let call = callCache.get(key);
|
|
354
354
|
if (!call) {
|
|
355
355
|
const authConfig: AuthConfig | undefined = connection.auth
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Each hook here caches its REST client per connection. Two connections to
|
|
2
|
+
// one bench that differ only in useToken must not share a client, or the
|
|
3
|
+
// first one built decides whether the other sends its token (MPO-399,
|
|
4
|
+
// MPO-406). The bench answers a tokenless call as Guest.
|
|
5
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { useFrappeReport } from "./FrappeReportView";
|
|
8
|
+
import { useFrappeAggregate } from "./useFrappeAggregate";
|
|
9
|
+
import { useFrappeFacets } from "./useFrappeFacets";
|
|
10
|
+
|
|
11
|
+
const token = "token key:secret";
|
|
12
|
+
let sent: (string | null)[] = [];
|
|
13
|
+
|
|
14
|
+
function reply(url: URL): unknown {
|
|
15
|
+
const method = url.pathname.replace(/^\/api\/method\//, "");
|
|
16
|
+
if (method === "frappe.client.get_count") return { message: 3 };
|
|
17
|
+
if (method === "frappe.desk.reportview.get_sidebar_stats") return { message: { stats: {} } };
|
|
18
|
+
if (method === "frappe.desk.query_report.run") return { message: { columns: [], result: [] } };
|
|
19
|
+
return { message: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
sent = [];
|
|
24
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request, init?: RequestInit) => {
|
|
25
|
+
sent.push(new Headers(init?.headers).get("Authorization"));
|
|
26
|
+
return new Response(JSON.stringify(reply(new URL(String(input)))), {
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
33
|
+
|
|
34
|
+
function connection(baseURL: string, useToken: boolean) {
|
|
35
|
+
return { baseURL, realtime: false, auth: { useToken, token: "key:secret", type: "token" } };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function authorizationPerConnection(
|
|
39
|
+
mount: (useToken: boolean) => () => boolean,
|
|
40
|
+
): Promise<{ off: (string | null)[]; on: (string | null)[] }> {
|
|
41
|
+
const offSettled = mount(false);
|
|
42
|
+
await waitFor(() => expect(offSettled()).toBe(true));
|
|
43
|
+
const off = sent.splice(0);
|
|
44
|
+
const onSettled = mount(true);
|
|
45
|
+
await waitFor(() => expect(onSettled()).toBe(true));
|
|
46
|
+
return { off, on: sent.splice(0) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("cached REST clients never drop a connection's token", () => {
|
|
50
|
+
it("useFrappeAggregate", async () => {
|
|
51
|
+
const { off, on } = await authorizationPerConnection((useToken) => {
|
|
52
|
+
const { result } = renderHook(() =>
|
|
53
|
+
useFrappeAggregate(connection("http://aggregate-auth.test", useToken), {
|
|
54
|
+
doctype: "ToDo",
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
return () => !result.current.isLoading;
|
|
58
|
+
});
|
|
59
|
+
expect(off).toEqual([null]);
|
|
60
|
+
expect(on).toEqual([token]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("useFrappeFacets", async () => {
|
|
64
|
+
const { off, on } = await authorizationPerConnection((useToken) => {
|
|
65
|
+
const { result } = renderHook(() =>
|
|
66
|
+
useFrappeFacets("ToDo", [], {
|
|
67
|
+
connection: connection("http://facets-auth.test", useToken),
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
return () =>
|
|
71
|
+
!result.current.assigned.isLoading &&
|
|
72
|
+
!result.current.owner.isLoading &&
|
|
73
|
+
!result.current.tags.isLoading;
|
|
74
|
+
});
|
|
75
|
+
expect(off.length).toBeGreaterThan(0);
|
|
76
|
+
expect(new Set(off)).toEqual(new Set([null]));
|
|
77
|
+
expect(on.length).toBe(off.length);
|
|
78
|
+
expect(new Set(on)).toEqual(new Set([token]));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("useFrappeReport (FrappeReportView)", async () => {
|
|
82
|
+
const { off, on } = await authorizationPerConnection((useToken) => {
|
|
83
|
+
const { result } = renderHook(() =>
|
|
84
|
+
useFrappeReport(connection("http://report-auth.test", useToken), {
|
|
85
|
+
reportName: "ToDo Summary",
|
|
86
|
+
debounceMs: 0,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
return () => !result.current.isLoading;
|
|
90
|
+
});
|
|
91
|
+
expect(off).toEqual([null]);
|
|
92
|
+
expect(on).toEqual([token]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -3,6 +3,8 @@ import { YStack } from "tamagui";
|
|
|
3
3
|
import { FrappeProvider } from "../FrappeProvider";
|
|
4
4
|
import { DeskFormSidebar } from "./DeskFormSidebar";
|
|
5
5
|
|
|
6
|
+
// hex-escape: generated image fixtures are data, not paint
|
|
7
|
+
|
|
6
8
|
export default {
|
|
7
9
|
title: "FrappeUI/Desk/DeskFormSidebar",
|
|
8
10
|
component: DeskFormSidebar,
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
2
2
|
import { action } from "@multiplatform.one/storybook";
|
|
3
3
|
import { Text, YStack } from "@multiplatform.one/components";
|
|
4
|
+
import { sizeRecipeEscape } from "@multiplatform.one/theme";
|
|
4
5
|
import { FrappeProvider } from "../FrappeProvider";
|
|
5
6
|
import { DeskShell } from "./DeskShell";
|
|
6
7
|
|
|
8
|
+
const frameHeight = sizeRecipeEscape(
|
|
9
|
+
640,
|
|
10
|
+
"story canvas: a fixed-height frame for a full-height screen to fill",
|
|
11
|
+
);
|
|
12
|
+
|
|
7
13
|
export default {
|
|
8
14
|
title: "FrappeUI/Desk/DeskShell",
|
|
9
15
|
component: DeskShell,
|
|
@@ -85,7 +91,7 @@ const content = (blocks: number) => (
|
|
|
85
91
|
|
|
86
92
|
export const Main = (args: Record<string, unknown>) => (
|
|
87
93
|
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({})}>
|
|
88
|
-
<YStack height={
|
|
94
|
+
<YStack height={frameHeight}>
|
|
89
95
|
<DeskShell
|
|
90
96
|
breadcrumbs={[{ label: "Desk", href: "/desk" }, { label: "Todo" }]}
|
|
91
97
|
onNavigate={action("onNavigate")}
|
|
@@ -102,7 +108,7 @@ Main.storyName = "Main";
|
|
|
102
108
|
|
|
103
109
|
export const NoSidebar = () => (
|
|
104
110
|
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({})}>
|
|
105
|
-
<YStack height={
|
|
111
|
+
<YStack height={frameHeight}>
|
|
106
112
|
<DeskShell showSidebar={false} onNavigate={action("onNavigate")}>
|
|
107
113
|
{content(2)}
|
|
108
114
|
</DeskShell>
|
|
@@ -112,7 +118,7 @@ export const NoSidebar = () => (
|
|
|
112
118
|
|
|
113
119
|
export const NoAwesomebar = () => (
|
|
114
120
|
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({})}>
|
|
115
|
-
<YStack height={
|
|
121
|
+
<YStack height={frameHeight}>
|
|
116
122
|
<DeskShell showAwesomebar={false} onNavigate={action("onNavigate")}>
|
|
117
123
|
{content(2)}
|
|
118
124
|
</DeskShell>
|
|
@@ -123,7 +129,7 @@ export const NoAwesomebar = () => (
|
|
|
123
129
|
/** Long page content must scroll inside the main area, header staying fixed. */
|
|
124
130
|
export const LongContent = () => (
|
|
125
131
|
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({})}>
|
|
126
|
-
<YStack height={
|
|
132
|
+
<YStack height={frameHeight}>
|
|
127
133
|
<DeskShell
|
|
128
134
|
breadcrumbs={[{ label: "Desk", href: "/desk" }, { label: "Long" }]}
|
|
129
135
|
onNavigate={action("onNavigate")}
|
|
@@ -136,7 +142,7 @@ export const LongContent = () => (
|
|
|
136
142
|
|
|
137
143
|
/** No breadcrumbs, no provider config: shell must still render header + nav + content. */
|
|
138
144
|
export const Bare = () => (
|
|
139
|
-
<YStack height={
|
|
145
|
+
<YStack height={frameHeight}>
|
|
140
146
|
<DeskShell onNavigate={action("onNavigate")}>{content(1)}</DeskShell>
|
|
141
147
|
</YStack>
|
|
142
148
|
);
|
|
@@ -144,7 +150,7 @@ export const Bare = () => (
|
|
|
144
150
|
/** Compact viewport: rail collapses to a phosphor toggle + overlay drawer. */
|
|
145
151
|
export const Narrow = () => (
|
|
146
152
|
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({})}>
|
|
147
|
-
<YStack height={
|
|
153
|
+
<YStack height={frameHeight}>
|
|
148
154
|
<DeskShell
|
|
149
155
|
breadcrumbs={[{ label: "Desk", href: "/desk" }, { label: "Todo" }]}
|
|
150
156
|
onNavigate={action("onNavigate")}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
useResolvedValidators,
|
|
30
30
|
type AnyFormApi,
|
|
31
31
|
} from "@multiplatform.one/forms";
|
|
32
|
-
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
32
|
+
import { sizeRecipeForToken, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
33
33
|
import { useEffect, useState, type ReactNode } from "react";
|
|
34
34
|
import { useTranslation } from "react-i18next";
|
|
35
35
|
import type { FontSizeTokens, LabelProps, SizeTokens } from "tamagui";
|
|
@@ -183,7 +183,13 @@ export function CodeEditorField({
|
|
|
183
183
|
if (skeleton) {
|
|
184
184
|
return (
|
|
185
185
|
<YStack {...knobProps.gap} width="100%">
|
|
186
|
-
{label &&
|
|
186
|
+
{label && (
|
|
187
|
+
<Skeleton
|
|
188
|
+
variant="text"
|
|
189
|
+
width="30%"
|
|
190
|
+
height={sizeRecipeForToken(knobProps.sizeToken).fontSize}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
187
193
|
<Skeleton variant="rounded" width="100%" height={compact ? 120 : 200} />
|
|
188
194
|
</YStack>
|
|
189
195
|
);
|
|
@@ -6,6 +6,8 @@ import { fieldtypeToTableField } from "../../fieldRegistry";
|
|
|
6
6
|
import { FrappeStoryProvider } from "../../frappeStoryTarget";
|
|
7
7
|
import { Color, type ColorProps } from ".";
|
|
8
8
|
|
|
9
|
+
// hex-escape: Color field values are hex strings: specimen data, not paint
|
|
10
|
+
|
|
9
11
|
export default {
|
|
10
12
|
title: "FrappeUI/Color",
|
|
11
13
|
component: Color,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useFrappeCollection } from "@multiplatform.one/frappe";
|
|
2
2
|
import { Skeleton } from "@multiplatform.one/forms";
|
|
3
3
|
import { FileIcon, UploadIcon, XIcon } from "@phosphor-icons/react";
|
|
4
|
-
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
4
|
+
import { sizeRecipeForToken, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
5
5
|
import type React from "react";
|
|
6
6
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
7
7
|
import { useTranslation } from "react-i18next";
|
|
@@ -276,7 +276,13 @@ export function FileUpload({
|
|
|
276
276
|
if (skeleton) {
|
|
277
277
|
return (
|
|
278
278
|
<YStack {...knobProps.gap}>
|
|
279
|
-
{label &&
|
|
279
|
+
{label && (
|
|
280
|
+
<Skeleton
|
|
281
|
+
variant="text"
|
|
282
|
+
width="30%"
|
|
283
|
+
height={sizeRecipeForToken(knobProps.sizeToken).fontSize}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
280
286
|
<Skeleton variant="rounded" width="100%" height={minHeight} />
|
|
281
287
|
</YStack>
|
|
282
288
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRegisterPageHeading } from "@multiplatform.one/components";
|
|
2
2
|
import { Skeleton } from "@multiplatform.one/forms";
|
|
3
|
-
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
3
|
+
import { sizeRecipeEscape, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
4
4
|
import type React from "react";
|
|
5
5
|
import { useCallback, useEffect, useId, useState } from "react";
|
|
6
6
|
import { H1, H2, H3, H4, Separator, Text, XStack, YStack, type YStackProps } from "tamagui";
|
|
@@ -105,7 +105,13 @@ export function Heading({
|
|
|
105
105
|
<YStack {...knobProps.gap} {...stackProps}>
|
|
106
106
|
<Skeleton variant="text" width="45%" height={compact ? 18 : 24} />
|
|
107
107
|
<Skeleton variant="text" width="70%" height={compact ? 12 : 14} />
|
|
108
|
-
{showSeparator &&
|
|
108
|
+
{showSeparator && (
|
|
109
|
+
<Skeleton
|
|
110
|
+
variant="rounded"
|
|
111
|
+
width="100%"
|
|
112
|
+
height={sizeRecipeEscape(1, "separator bone: the 1px rule a Separator draws")}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
109
115
|
</YStack>
|
|
110
116
|
);
|
|
111
117
|
}
|
|
@@ -31,7 +31,7 @@ import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
|
|
31
31
|
import { useTranslation } from "react-i18next";
|
|
32
32
|
import type { FontSizeTokens, LabelProps, SizeTokens } from "tamagui";
|
|
33
33
|
import { Paragraph, YStack } from "tamagui";
|
|
34
|
-
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
34
|
+
import { sizeRecipeForToken, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
35
35
|
import { EditorPreviewPanes, PreviewSurface } from "../shared/EditorPreviewPanes";
|
|
36
36
|
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
@@ -176,7 +176,13 @@ export function MarkdownEditorField({
|
|
|
176
176
|
if (skeleton) {
|
|
177
177
|
return (
|
|
178
178
|
<YStack {...knobProps.gap} width="100%">
|
|
179
|
-
{label &&
|
|
179
|
+
{label && (
|
|
180
|
+
<Skeleton
|
|
181
|
+
variant="text"
|
|
182
|
+
width="30%"
|
|
183
|
+
height={sizeRecipeForToken(knobProps.sizeToken).fontSize}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
180
186
|
<Skeleton variant="rounded" width="100%" height={compact ? 120 : 200} />
|
|
181
187
|
</YStack>
|
|
182
188
|
);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type AnyFormApi,
|
|
28
28
|
} from "@multiplatform.one/forms";
|
|
29
29
|
import { RichEditor, type RichEditorRef } from "@multiplatform.one/rich-text";
|
|
30
|
+
import { sizeRecipeForToken } from "@multiplatform.one/theme";
|
|
30
31
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
31
32
|
import type { LabelProps, SizeTokens } from "tamagui";
|
|
32
33
|
import { YStack } from "tamagui";
|
|
@@ -111,7 +112,13 @@ export function MarkdownEditorField({
|
|
|
111
112
|
if (skeleton) {
|
|
112
113
|
return (
|
|
113
114
|
<YStack {...knobProps.gap} width="100%">
|
|
114
|
-
{label &&
|
|
115
|
+
{label && (
|
|
116
|
+
<Skeleton
|
|
117
|
+
variant="text"
|
|
118
|
+
width="30%"
|
|
119
|
+
height={sizeRecipeForToken(knobProps.sizeToken).fontSize}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
115
122
|
<Skeleton variant="rounded" width="100%" height={compact ? 120 : 200} />
|
|
116
123
|
</YStack>
|
|
117
124
|
);
|
|
@@ -4,6 +4,8 @@ import { fieldtypeToTableField } from "../../fieldRegistry";
|
|
|
4
4
|
import { FrappeStoryProvider } from "../../frappeStoryTarget";
|
|
5
5
|
import { ReadOnly } from ".";
|
|
6
6
|
|
|
7
|
+
// hex-escape: Color field values are hex strings: specimen data, not paint
|
|
8
|
+
|
|
7
9
|
export default {
|
|
8
10
|
title: "FrappeUI/ReadOnly",
|
|
9
11
|
component: ReadOnly,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// A host built with Frappe off (MPO-403): createApp mounts no FrappeProvider
|
|
2
|
+
// without FRAPPE_ENABLED, so a doctype's routed pages find no bench URL and no
|
|
3
|
+
// fixture corpus. They say the backend isn't configured instead of throwing
|
|
4
|
+
// and taking the route down, and they dial nothing.
|
|
5
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
6
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
7
|
+
import i18next from "i18next";
|
|
8
|
+
import { initReactI18next } from "react-i18next";
|
|
9
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { FrappeForm } from "./FrappeForm";
|
|
11
|
+
import { FrappeListView } from "./FrappeListView";
|
|
12
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
13
|
+
|
|
14
|
+
interface Move {
|
|
15
|
+
name: string;
|
|
16
|
+
doctype: string;
|
|
17
|
+
move_name?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const doctype = "Poke Move";
|
|
21
|
+
const route = "/moves";
|
|
22
|
+
|
|
23
|
+
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await i18next.use(initReactI18next).init({
|
|
27
|
+
lng: "en",
|
|
28
|
+
interpolation: { escapeValue: false },
|
|
29
|
+
resources: { en: { translation: {} } },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
fetchSpy = vi.fn(async () => {
|
|
35
|
+
throw new Error("a page with no bench dialled one");
|
|
36
|
+
});
|
|
37
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
cleanup();
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
vi.unstubAllGlobals();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function expectNotConfigured() {
|
|
47
|
+
expect(screen.getByTestId("frappe-not-configured")).toBeTruthy();
|
|
48
|
+
expect(screen.getByText("Frappe is not configured")).toBeTruthy();
|
|
49
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("a doctype's pages on a host with no Frappe backend", () => {
|
|
53
|
+
it("the new page says the backend isn't configured", () => {
|
|
54
|
+
renderWithProviders(<FrappeForm<Move> doctype={doctype} route={route} />);
|
|
55
|
+
expectNotConfigured();
|
|
56
|
+
expect(screen.getByText(`${doctype} can't load without a Frappe bench URL.`)).toBeTruthy();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("a record page says the backend isn't configured", () => {
|
|
60
|
+
renderWithProviders(<FrappeForm<Move> doctype={doctype} name="a1b2c3" />);
|
|
61
|
+
expectNotConfigured();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("the list page says the backend isn't configured", () => {
|
|
65
|
+
renderWithProviders(<FrappeListView<Move> doctype={doctype} route={route} />);
|
|
66
|
+
expectNotConfigured();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("a provider that resolved no bench URL reads the same", () => {
|
|
70
|
+
renderWithProviders(
|
|
71
|
+
<FrappeProvider realtime={false}>
|
|
72
|
+
<FrappeForm<Move> doctype={doctype} route={route} />
|
|
73
|
+
</FrappeProvider>,
|
|
74
|
+
);
|
|
75
|
+
expectNotConfigured();
|
|
76
|
+
});
|
|
77
|
+
});
|