@multiplatform.one/frappe-ui 7.6.1 → 7.6.3
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/FileBrowser/AttachLibrary.cjs +51 -49
- package/dist/cjs/FileBrowser/AttachLibrary.native.js +58 -49
- package/dist/cjs/FileBrowser/AttachLibrary.native.js.map +1 -1
- package/dist/cjs/FrappeConnectionIndicator.cjs +5 -3
- package/dist/cjs/FrappeConnectionIndicator.native.js +5 -3
- package/dist/cjs/FrappeConnectionIndicator.native.js.map +1 -1
- package/dist/cjs/FrappeForm.cjs +3 -1
- package/dist/cjs/FrappeForm.native.js +3 -1
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeReconnectIndicator.cjs +4 -2
- package/dist/cjs/FrappeReconnectIndicator.native.js +4 -2
- package/dist/cjs/FrappeReconnectIndicator.native.js.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.native.js +2 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/schemaForm/applyFieldOrder.cjs +63 -0
- package/dist/cjs/schemaForm/applyFieldOrder.native.js +116 -0
- package/dist/cjs/schemaForm/applyFieldOrder.native.js.map +1 -0
- package/dist/cjs/schemaForm/index.cjs +13 -7
- package/dist/cjs/schemaForm/index.native.js +15 -7
- package/dist/cjs/schemaForm/index.native.js.map +1 -1
- package/dist/esm/FileBrowser/AttachLibrary.mjs +51 -49
- package/dist/esm/FileBrowser/AttachLibrary.mjs.map +1 -1
- package/dist/esm/FileBrowser/AttachLibrary.native.js +58 -49
- package/dist/esm/FileBrowser/AttachLibrary.native.js.map +1 -1
- package/dist/esm/FrappeConnectionIndicator.mjs +5 -3
- package/dist/esm/FrappeConnectionIndicator.mjs.map +1 -1
- package/dist/esm/FrappeConnectionIndicator.native.js +5 -3
- package/dist/esm/FrappeConnectionIndicator.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +3 -1
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +3 -1
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeReconnectIndicator.mjs +4 -2
- package/dist/esm/FrappeReconnectIndicator.mjs.map +1 -1
- package/dist/esm/FrappeReconnectIndicator.native.js +4 -2
- package/dist/esm/FrappeReconnectIndicator.native.js.map +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/schemaForm/applyFieldOrder.mjs +37 -0
- package/dist/esm/schemaForm/applyFieldOrder.mjs.map +1 -0
- package/dist/esm/schemaForm/applyFieldOrder.native.js +87 -0
- package/dist/esm/schemaForm/applyFieldOrder.native.js.map +1 -0
- package/dist/esm/schemaForm/index.mjs +11 -7
- package/dist/esm/schemaForm/index.mjs.map +1 -1
- package/dist/esm/schemaForm/index.native.js +13 -7
- package/dist/esm/schemaForm/index.native.js.map +1 -1
- package/dist/jsx/FileBrowser/AttachLibrary.mjs +51 -49
- package/dist/jsx/FileBrowser/AttachLibrary.mjs.map +1 -1
- package/dist/jsx/FileBrowser/AttachLibrary.native.js +58 -49
- package/dist/jsx/FileBrowser/AttachLibrary.native.js.map +1 -1
- package/dist/jsx/FrappeConnectionIndicator.mjs +5 -3
- package/dist/jsx/FrappeConnectionIndicator.mjs.map +1 -1
- package/dist/jsx/FrappeConnectionIndicator.native.js +5 -3
- package/dist/jsx/FrappeConnectionIndicator.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +3 -1
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +3 -1
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeReconnectIndicator.mjs +4 -2
- package/dist/jsx/FrappeReconnectIndicator.mjs.map +1 -1
- package/dist/jsx/FrappeReconnectIndicator.native.js +4 -2
- package/dist/jsx/FrappeReconnectIndicator.native.js.map +1 -1
- package/dist/jsx/index.js +2 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +2 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/schemaForm/applyFieldOrder.mjs +37 -0
- package/dist/jsx/schemaForm/applyFieldOrder.mjs.map +1 -0
- package/dist/jsx/schemaForm/applyFieldOrder.native.js +116 -0
- package/dist/jsx/schemaForm/applyFieldOrder.native.js.map +1 -0
- package/dist/jsx/schemaForm/index.mjs +11 -7
- package/dist/jsx/schemaForm/index.mjs.map +1 -1
- package/dist/jsx/schemaForm/index.native.js +15 -7
- package/dist/jsx/schemaForm/index.native.js.map +1 -1
- package/package.json +16 -16
- package/src/FileBrowser/AttachLibrary.spec.tsx +74 -0
- package/src/FileBrowser/AttachLibrary.stories.tsx +67 -0
- package/src/FileBrowser/AttachLibrary.tsx +56 -49
- package/src/FrappeConnectionIndicator.spec.tsx +51 -0
- package/src/FrappeConnectionIndicator.stories.tsx +55 -0
- package/src/FrappeConnectionIndicator.tsx +15 -4
- package/src/FrappeForm.tsx +13 -3
- package/src/FrappeReconnectIndicator.spec.tsx +52 -0
- package/src/FrappeReconnectIndicator.stories.tsx +55 -0
- package/src/FrappeReconnectIndicator.tsx +13 -3
- package/src/SyncErrorNotice.stories.tsx +50 -0
- package/src/desk/DeskFormSidebar.spec.tsx +90 -0
- package/src/desk/DeskFormSidebar.stories.tsx +133 -0
- package/src/fields/LinkTree.spec.tsx +109 -0
- package/src/fields/LinkTree.stories.tsx +187 -0
- package/src/index.ts +1 -1
- package/src/schemaForm/SchemaForm.spec.tsx +29 -0
- package/src/schemaForm/SchemaForm.stories.tsx +14 -1
- package/src/schemaForm/applyFieldOrder.spec.ts +75 -0
- package/src/schemaForm/applyFieldOrder.ts +71 -0
- package/src/schemaForm/index.tsx +17 -5
- package/types/FileBrowser/AttachLibrary.d.ts.map +1 -1
- package/types/FrappeConnectionIndicator.d.ts +8 -2
- package/types/FrappeConnectionIndicator.d.ts.map +1 -1
- package/types/FrappeForm.d.ts +7 -1
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeReconnectIndicator.d.ts +7 -2
- package/types/FrappeReconnectIndicator.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +1 -1
- package/types/schemaForm/applyFieldOrder.d.ts +28 -0
- package/types/schemaForm/applyFieldOrder.d.ts.map +1 -0
- package/types/schemaForm/index.d.ts +8 -1
- package/types/schemaForm/index.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/frappe-ui",
|
|
3
|
-
"version": "7.6.
|
|
3
|
+
"version": "7.6.3",
|
|
4
4
|
"description": "Pre-wired Tamagui components for Frappe doctypes with live data",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cross-platform",
|
|
@@ -78,18 +78,18 @@
|
|
|
78
78
|
"html5-qrcode": "^2.3.8",
|
|
79
79
|
"signature_pad": "^5.1.3",
|
|
80
80
|
"tamagui": "2.7.6",
|
|
81
|
-
"@multiplatform.one/components": "7.6.
|
|
82
|
-
"@multiplatform.one/desktop": "7.6.
|
|
83
|
-
"@multiplatform.one/forms": "7.6.
|
|
84
|
-
"@multiplatform.one/frappe": "7.6.
|
|
85
|
-
"@multiplatform.one/i18n": "7.6.
|
|
86
|
-
"@multiplatform.one/markdown": "7.6.
|
|
87
|
-
"@multiplatform.one/platform": "7.6.
|
|
88
|
-
"@multiplatform.one/rich-text": "7.6.
|
|
89
|
-
"@multiplatform.one/router": "7.6.
|
|
90
|
-
"@multiplatform.one/store": "7.6.
|
|
91
|
-
"@multiplatform.one/table": "7.6.
|
|
92
|
-
"@multiplatform.one/theme": "7.6.
|
|
81
|
+
"@multiplatform.one/components": "7.6.3",
|
|
82
|
+
"@multiplatform.one/desktop": "7.6.3",
|
|
83
|
+
"@multiplatform.one/forms": "7.6.3",
|
|
84
|
+
"@multiplatform.one/frappe": "7.6.3",
|
|
85
|
+
"@multiplatform.one/i18n": "7.6.3",
|
|
86
|
+
"@multiplatform.one/markdown": "7.6.3",
|
|
87
|
+
"@multiplatform.one/platform": "7.6.3",
|
|
88
|
+
"@multiplatform.one/rich-text": "7.6.3",
|
|
89
|
+
"@multiplatform.one/router": "7.6.3",
|
|
90
|
+
"@multiplatform.one/store": "7.6.3",
|
|
91
|
+
"@multiplatform.one/table": "7.6.3",
|
|
92
|
+
"@multiplatform.one/theme": "7.6.3"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@tamagui/build": "2.7.6",
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
"react-dom": "19.2.5",
|
|
103
103
|
"react-i18next": "^16.6.6",
|
|
104
104
|
"vitest": "^4.1.5",
|
|
105
|
-
"@multiplatform.one/config": "7.6.
|
|
106
|
-
"@multiplatform.one/storybook": "7.6.
|
|
107
|
-
"@multiplatform.one/test-utils": "7.6.
|
|
105
|
+
"@multiplatform.one/config": "7.6.3",
|
|
106
|
+
"@multiplatform.one/storybook": "7.6.3",
|
|
107
|
+
"@multiplatform.one/test-utils": "7.6.3"
|
|
108
108
|
},
|
|
109
109
|
"peerDependencies": {
|
|
110
110
|
"@react-native-community/datetimepicker": ">=8.0.1",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
3
|
+
import i18next from "i18next";
|
|
4
|
+
import { initReactI18next } from "react-i18next";
|
|
5
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { AttachLibrary } from "./AttachLibrary";
|
|
7
|
+
import type { FileBrowserFile } from "./types";
|
|
8
|
+
|
|
9
|
+
const FIXTURE: FileBrowserFile[] = [
|
|
10
|
+
{ name: "Home", file_name: "Home", folder: "", is_folder: 1 },
|
|
11
|
+
{ name: "Home/Invoices", file_name: "Invoices", folder: "Home", is_folder: 1 },
|
|
12
|
+
{
|
|
13
|
+
name: "inv-1",
|
|
14
|
+
file_name: "invoice.pdf",
|
|
15
|
+
folder: "Home/Invoices",
|
|
16
|
+
file_size: 72400,
|
|
17
|
+
file_type: "pdf",
|
|
18
|
+
file_url: "/files/invoice.pdf",
|
|
19
|
+
modified: "2026-08-12",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "img-1",
|
|
23
|
+
file_name: "topology.png",
|
|
24
|
+
folder: "Home/Invoices",
|
|
25
|
+
file_size: 612000,
|
|
26
|
+
file_type: "png",
|
|
27
|
+
file_url: "/files/topology.png",
|
|
28
|
+
modified: "2026-08-09",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
await i18next.use(initReactI18next).init({
|
|
34
|
+
lng: "en",
|
|
35
|
+
interpolation: { escapeValue: false },
|
|
36
|
+
resources: { en: { translation: {} } },
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("AttachLibrary (MPO-111)", () => {
|
|
41
|
+
it("opens the library tab onto FileBrowser and commits onPick", () => {
|
|
42
|
+
const onPick = vi.fn();
|
|
43
|
+
const onOpenChange = vi.fn();
|
|
44
|
+
renderWithProviders(
|
|
45
|
+
<AttachLibrary
|
|
46
|
+
open
|
|
47
|
+
onOpenChange={onOpenChange}
|
|
48
|
+
files={FIXTURE}
|
|
49
|
+
currentFolder="Home/Invoices"
|
|
50
|
+
onPick={onPick}
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
expect(screen.getByRole("dialog", { name: "Attach file" })).toBeTruthy();
|
|
54
|
+
expect(screen.getByRole("tab", { name: /From library/i })).toBeTruthy();
|
|
55
|
+
expect(document.querySelector("[data-filebrowser-attach-library]")).toBeTruthy();
|
|
56
|
+
expect(screen.getByText("invoice.pdf")).toBeTruthy();
|
|
57
|
+
fireEvent.click(screen.getByText("invoice.pdf"));
|
|
58
|
+
expect(onPick).toHaveBeenCalled();
|
|
59
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders nothing interactive when closed", () => {
|
|
63
|
+
renderWithProviders(
|
|
64
|
+
<AttachLibrary
|
|
65
|
+
open={false}
|
|
66
|
+
onOpenChange={vi.fn()}
|
|
67
|
+
files={FIXTURE}
|
|
68
|
+
currentFolder="Home/Invoices"
|
|
69
|
+
onPick={vi.fn()}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { YStack } from "tamagui";
|
|
3
|
+
import { AttachLibrary } from "./AttachLibrary";
|
|
4
|
+
import type { FileBrowserFile } from "./types";
|
|
5
|
+
|
|
6
|
+
const files: FileBrowserFile[] = [
|
|
7
|
+
{ name: "Home", file_name: "Home", folder: "", is_folder: 1 },
|
|
8
|
+
{ name: "Home/Invoices", file_name: "Invoices", folder: "Home", is_folder: 1 },
|
|
9
|
+
{
|
|
10
|
+
name: "inv-1",
|
|
11
|
+
file_name: "hetzner-hel1-invoice-aug.pdf",
|
|
12
|
+
folder: "Home/Invoices",
|
|
13
|
+
file_size: 72400,
|
|
14
|
+
file_type: "pdf",
|
|
15
|
+
file_url: "/files/invoice.pdf",
|
|
16
|
+
modified: "2026-08-12",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "img-1",
|
|
20
|
+
file_name: "swarm01-topology.png",
|
|
21
|
+
folder: "Home/Invoices",
|
|
22
|
+
file_size: 612000,
|
|
23
|
+
file_type: "png",
|
|
24
|
+
file_url: "/files/topology.png",
|
|
25
|
+
modified: "2026-08-09",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
title: "FrappeUI/AttachLibrary",
|
|
31
|
+
component: AttachLibrary,
|
|
32
|
+
parameters: {
|
|
33
|
+
status: { type: "beta" },
|
|
34
|
+
docs: {
|
|
35
|
+
description: {
|
|
36
|
+
component:
|
|
37
|
+
'Attach dialog: Upload tab plus a FileBrowser "From library" tab. FileUploadField opens this; picking a library row commits and closes.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Default = () => {
|
|
44
|
+
const [open, setOpen] = useState(true);
|
|
45
|
+
return (
|
|
46
|
+
<YStack height={520}>
|
|
47
|
+
<AttachLibrary
|
|
48
|
+
open={open}
|
|
49
|
+
onOpenChange={setOpen}
|
|
50
|
+
files={files}
|
|
51
|
+
currentFolder="Home/Invoices"
|
|
52
|
+
onPick={() => setOpen(false)}
|
|
53
|
+
/>
|
|
54
|
+
</YStack>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
Default.storyName = "Main";
|
|
58
|
+
|
|
59
|
+
export const Closed = () => (
|
|
60
|
+
<AttachLibrary
|
|
61
|
+
open={false}
|
|
62
|
+
onOpenChange={() => {}}
|
|
63
|
+
files={files}
|
|
64
|
+
currentFolder="Home/Invoices"
|
|
65
|
+
onPick={() => {}}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
@@ -36,55 +36,62 @@ export function AttachLibrary({
|
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
38
|
<Dialog modal open={open} onOpenChange={onOpenChange}>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
{t("Attach file")}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
39
|
+
{/* Dialog.Portal is what gates mounting on `open`. Without it Tamagui
|
|
40
|
+
mounts Dialog.Content unconditionally, so a CLOSED AttachLibrary still
|
|
41
|
+
put role="dialog" in the a11y tree and kept the whole FileBrowser
|
|
42
|
+
subtree live. Every other house dialog (ConfirmDialog, CellModalEditor,
|
|
43
|
+
HelpAboutDialog) already wraps overlay + content this way. */}
|
|
44
|
+
<Dialog.Portal>
|
|
45
|
+
<DialogOverlay backgroundColor="$background" opacity={0.5} data-filebrowser-attach-scrim />
|
|
46
|
+
<DialogContent
|
|
47
|
+
aria-label={t("Attach file")}
|
|
48
|
+
data-filebrowser-attach-dialog
|
|
49
|
+
width={drill ? "100%" : 820}
|
|
50
|
+
maxWidth="100%"
|
|
51
|
+
>
|
|
52
|
+
<Text fontSize={20} lineHeight={32} fontFamily="$mono">
|
|
53
|
+
{t("Attach file")}
|
|
54
|
+
</Text>
|
|
55
|
+
<Tabs
|
|
56
|
+
variant="contained"
|
|
57
|
+
value={tab}
|
|
58
|
+
onChange={setTab}
|
|
59
|
+
items={[
|
|
60
|
+
{
|
|
61
|
+
value: "upload",
|
|
62
|
+
label: t("Upload"),
|
|
63
|
+
content: (
|
|
64
|
+
<YStack paddingTop="$4">
|
|
65
|
+
<FileUpload accept={accept} />
|
|
66
|
+
</YStack>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
value: "library",
|
|
71
|
+
label: t("From library"),
|
|
72
|
+
content: (
|
|
73
|
+
<YStack paddingTop="$4" data-filebrowser-attach-library>
|
|
74
|
+
<FileBrowser
|
|
75
|
+
files={files}
|
|
76
|
+
layout={drill ? "drill" : "split"}
|
|
77
|
+
selectionMode="single"
|
|
78
|
+
currentValue={current}
|
|
79
|
+
currentFolder={currentFolder}
|
|
80
|
+
surfaceId="attach-library"
|
|
81
|
+
onPick={(file) => {
|
|
82
|
+
onPick(file);
|
|
83
|
+
onOpenChange(false);
|
|
84
|
+
}}
|
|
85
|
+
accept={accept}
|
|
86
|
+
onUpload={onUpload}
|
|
87
|
+
/>
|
|
88
|
+
</YStack>
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
</DialogContent>
|
|
94
|
+
</Dialog.Portal>
|
|
88
95
|
</Dialog>
|
|
89
96
|
);
|
|
90
97
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
3
|
+
import i18next from "i18next";
|
|
4
|
+
import { initReactI18next } from "react-i18next";
|
|
5
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
|
+
import { FrappeConnectionIndicator } from "./FrappeConnectionIndicator";
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
await i18next.use(initReactI18next).init({
|
|
10
|
+
lng: "en",
|
|
11
|
+
interpolation: { escapeValue: false },
|
|
12
|
+
resources: { en: { translation: {} } },
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup);
|
|
17
|
+
|
|
18
|
+
const config = { baseURL: "https://bench.example.com" };
|
|
19
|
+
|
|
20
|
+
describe("FrappeConnectionIndicator (MPO-111)", () => {
|
|
21
|
+
it("renders nothing when the socket was never up", () => {
|
|
22
|
+
const { container } = renderWithProviders(
|
|
23
|
+
<FrappeConnectionIndicator
|
|
24
|
+
config={config}
|
|
25
|
+
connection={{ connected: false, everConnected: false }}
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
expect(container.querySelector('[role="status"]')).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders nothing while connected", () => {
|
|
32
|
+
const { container } = renderWithProviders(
|
|
33
|
+
<FrappeConnectionIndicator
|
|
34
|
+
config={config}
|
|
35
|
+
connection={{ connected: true, everConnected: true }}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
expect(container.querySelector('[role="status"]')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders a live status pill while reconnecting", async () => {
|
|
42
|
+
renderWithProviders(
|
|
43
|
+
<FrappeConnectionIndicator
|
|
44
|
+
config={config}
|
|
45
|
+
connection={{ connected: false, everConnected: true }}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
const status = await screen.findByRole("status");
|
|
49
|
+
expect(status).toHaveTextContent("Reconnecting…");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Text, YStack } from "tamagui";
|
|
2
|
+
import {
|
|
3
|
+
FrappeConnectionIndicator,
|
|
4
|
+
type FrappeConnectionIndicatorProps,
|
|
5
|
+
} from "./FrappeConnectionIndicator";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: "FrappeUI/FrappeConnectionIndicator",
|
|
9
|
+
component: FrappeConnectionIndicator,
|
|
10
|
+
parameters: {
|
|
11
|
+
status: { type: "stable" },
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
"Compact reconnecting pill for live Frappe views. Renders only while the realtime socket is down after having been up. Fixture mode, SSR, and a healthy socket all render nothing.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const config: FrappeConnectionIndicatorProps["config"] = {
|
|
22
|
+
baseURL: "https://connection.mock.test",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Default = () => (
|
|
26
|
+
<YStack gap="$3" width={360}>
|
|
27
|
+
<Text fontSize="$2" color="$color10">
|
|
28
|
+
Fixture / never-connected: the indicator paints nothing. That is the contract.
|
|
29
|
+
</Text>
|
|
30
|
+
<FrappeConnectionIndicator
|
|
31
|
+
config={config}
|
|
32
|
+
connection={{ connected: false, everConnected: false }}
|
|
33
|
+
/>
|
|
34
|
+
</YStack>
|
|
35
|
+
);
|
|
36
|
+
Default.storyName = "Main";
|
|
37
|
+
|
|
38
|
+
export const Reconnecting = () => (
|
|
39
|
+
<FrappeConnectionIndicator
|
|
40
|
+
config={config}
|
|
41
|
+
connection={{ connected: false, everConnected: true }}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const Connected = () => (
|
|
46
|
+
<YStack gap="$3" width={360}>
|
|
47
|
+
<Text fontSize="$2" color="$color10">
|
|
48
|
+
Live socket up: nothing to show.
|
|
49
|
+
</Text>
|
|
50
|
+
<FrappeConnectionIndicator
|
|
51
|
+
config={config}
|
|
52
|
+
connection={{ connected: true, everConnected: true }}
|
|
53
|
+
/>
|
|
54
|
+
</YStack>
|
|
55
|
+
);
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useFrappeConnection,
|
|
3
|
+
type FrappeConnectionState,
|
|
4
|
+
type UseFrappeCollectionConfig,
|
|
5
|
+
} from "@multiplatform.one/frappe";
|
|
6
|
+
import { radiusClassProps, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
2
7
|
import React from "react";
|
|
3
8
|
import { useTranslation } from "react-i18next";
|
|
4
9
|
import { Text, XStack, YStack } from "tamagui";
|
|
5
|
-
import { radiusClassProps, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
6
10
|
|
|
7
11
|
export interface FrappeConnectionIndicatorProps {
|
|
8
12
|
/** The same connection config the sibling collection hooks use. */
|
|
9
13
|
config: UseFrappeCollectionConfig | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Story and spec injection. Production callers omit this; the live socket
|
|
16
|
+
* from `useFrappeConnection(config)` wins. Fixture configs always report
|
|
17
|
+
* never-connected, so boards that need the reconnecting pill pass a snapshot.
|
|
18
|
+
*/
|
|
19
|
+
connection?: FrappeConnectionState;
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
/**
|
|
@@ -18,10 +28,11 @@ export interface FrappeConnectionIndicatorProps {
|
|
|
18
28
|
* connected, in fixture mode, or during SSR (realtime never up), so
|
|
19
29
|
* fixture-backed stories stay clean.
|
|
20
30
|
*/
|
|
21
|
-
export function FrappeConnectionIndicator({ config }: FrappeConnectionIndicatorProps) {
|
|
31
|
+
export function FrappeConnectionIndicator({ config, connection }: FrappeConnectionIndicatorProps) {
|
|
22
32
|
const { t } = useTranslation();
|
|
23
33
|
const { knobProps } = useResolvedKnobs();
|
|
24
|
-
const
|
|
34
|
+
const hooked = useFrappeConnection(config);
|
|
35
|
+
const { connected, everConnected } = connection ?? hooked;
|
|
25
36
|
if (connected || !everConnected) return null;
|
|
26
37
|
return (
|
|
27
38
|
<XStack
|
package/src/FrappeForm.tsx
CHANGED
|
@@ -66,6 +66,12 @@ export interface FrappeFormProps<TDoc extends { name: string; doctype: string }>
|
|
|
66
66
|
readOnlyFields?: string[];
|
|
67
67
|
/** Completely replace the auto-generated layout. */
|
|
68
68
|
sections?: FormSection[];
|
|
69
|
+
/**
|
|
70
|
+
* R2: reorder fields in the meta-derived layout. Listed names permute
|
|
71
|
+
* among their slots; unlisted fields keep their meta positions. Unknown
|
|
72
|
+
* names are ignored. Cheaper than replacing `sections`.
|
|
73
|
+
*/
|
|
74
|
+
order?: string[];
|
|
69
75
|
onSave?: (doc: Partial<TDoc>) => void;
|
|
70
76
|
onCancel?: () => void;
|
|
71
77
|
onDelete?: (doc: Partial<TDoc>) => void;
|
|
@@ -117,9 +123,11 @@ function mapMetaToFormSchema(meta: DocTypeMeta, sectionOverrides?: FormSection[]
|
|
|
117
123
|
const genericSections: GenericFormSection[] = sections.map((section) => ({
|
|
118
124
|
label: section.label,
|
|
119
125
|
collapsible: section.collapsible,
|
|
120
|
-
columns: section.columns.map(
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
columns: section.columns.map(
|
|
127
|
+
(col): GenericFormColumn => ({
|
|
128
|
+
fields: col.fields.map(mapFieldToGenericSchema),
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
123
131
|
}));
|
|
124
132
|
|
|
125
133
|
// Flat list of all fields for depends_on pre-compilation
|
|
@@ -274,6 +282,7 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
274
282
|
hideFields,
|
|
275
283
|
readOnlyFields,
|
|
276
284
|
sections,
|
|
285
|
+
order,
|
|
277
286
|
onSave,
|
|
278
287
|
onCancel,
|
|
279
288
|
onDelete,
|
|
@@ -765,6 +774,7 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
765
774
|
hideFields={hideFields}
|
|
766
775
|
readOnlyFields={readOnlyFields}
|
|
767
776
|
sectionHeadingLevel={sectionHeadingLevel}
|
|
777
|
+
order={order}
|
|
768
778
|
/>
|
|
769
779
|
) : (
|
|
770
780
|
// Metadata unavailable (should be covered by metaError branch above)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
3
|
+
import i18next from "i18next";
|
|
4
|
+
import { initReactI18next } from "react-i18next";
|
|
5
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
|
+
import { FrappeReconnectIndicator } from "./FrappeReconnectIndicator";
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
await i18next.use(initReactI18next).init({
|
|
10
|
+
lng: "en",
|
|
11
|
+
interpolation: { escapeValue: false },
|
|
12
|
+
resources: { en: { translation: {} } },
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup);
|
|
17
|
+
|
|
18
|
+
const config = { baseURL: "https://bench.example.com" };
|
|
19
|
+
|
|
20
|
+
describe("FrappeReconnectIndicator (MPO-111)", () => {
|
|
21
|
+
it("renders nothing when the socket was never up", () => {
|
|
22
|
+
const { container } = renderWithProviders(
|
|
23
|
+
<FrappeReconnectIndicator
|
|
24
|
+
config={config}
|
|
25
|
+
connection={{ connected: false, everConnected: false }}
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
expect(container.querySelector('[data-testid="frappe-reconnect-indicator"]')).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders nothing while connected", () => {
|
|
32
|
+
const { container } = renderWithProviders(
|
|
33
|
+
<FrappeReconnectIndicator
|
|
34
|
+
config={config}
|
|
35
|
+
connection={{ connected: true, everConnected: true }}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
expect(container.querySelector('[data-testid="frappe-reconnect-indicator"]')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders the interrupted-updates chip while reconnecting", async () => {
|
|
42
|
+
renderWithProviders(
|
|
43
|
+
<FrappeReconnectIndicator
|
|
44
|
+
config={config}
|
|
45
|
+
connection={{ connected: false, everConnected: true }}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
const chip = await screen.findByTestId("frappe-reconnect-indicator");
|
|
49
|
+
expect(chip.getAttribute("role")).toBe("status");
|
|
50
|
+
expect(chip).toHaveTextContent("Live updates interrupted — reconnecting…");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Text, YStack } from "tamagui";
|
|
2
|
+
import {
|
|
3
|
+
FrappeReconnectIndicator,
|
|
4
|
+
type FrappeReconnectIndicatorProps,
|
|
5
|
+
} from "./FrappeReconnectIndicator";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: "FrappeUI/FrappeReconnectIndicator",
|
|
9
|
+
component: FrappeReconnectIndicator,
|
|
10
|
+
parameters: {
|
|
11
|
+
status: { type: "stable" },
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
"Honest realtime-state chip for live Frappe components. Renders only while the socket feeding this connection's live queries is down after having been up. Fixture-backed configs render nothing.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const config: FrappeReconnectIndicatorProps["config"] = {
|
|
22
|
+
baseURL: "https://reconnect.mock.test",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Default = () => (
|
|
26
|
+
<YStack gap="$3" width={420}>
|
|
27
|
+
<Text fontSize="$2" color="$color10">
|
|
28
|
+
Never-connected (fixtures, SSR): paints nothing.
|
|
29
|
+
</Text>
|
|
30
|
+
<FrappeReconnectIndicator
|
|
31
|
+
config={config}
|
|
32
|
+
connection={{ connected: false, everConnected: false }}
|
|
33
|
+
/>
|
|
34
|
+
</YStack>
|
|
35
|
+
);
|
|
36
|
+
Default.storyName = "Main";
|
|
37
|
+
|
|
38
|
+
export const Reconnecting = () => (
|
|
39
|
+
<FrappeReconnectIndicator
|
|
40
|
+
config={config}
|
|
41
|
+
connection={{ connected: false, everConnected: true }}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const Connected = () => (
|
|
46
|
+
<YStack gap="$3" width={420}>
|
|
47
|
+
<Text fontSize="$2" color="$color10">
|
|
48
|
+
Socket up: the chip is gone.
|
|
49
|
+
</Text>
|
|
50
|
+
<FrappeReconnectIndicator
|
|
51
|
+
config={config}
|
|
52
|
+
connection={{ connected: true, everConnected: true }}
|
|
53
|
+
/>
|
|
54
|
+
</YStack>
|
|
55
|
+
);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useFrappeConnection,
|
|
3
|
+
type FrappeConnectionState,
|
|
4
|
+
type UseFrappeCollectionConfig,
|
|
5
|
+
} from "@multiplatform.one/frappe";
|
|
2
6
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
3
7
|
import { WifiSlashIcon } from "@phosphor-icons/react";
|
|
4
8
|
import { useTranslation } from "react-i18next";
|
|
@@ -7,6 +11,11 @@ import { Text, XStack } from "tamagui";
|
|
|
7
11
|
export interface FrappeReconnectIndicatorProps {
|
|
8
12
|
/** The connection config the component's live queries sync through. */
|
|
9
13
|
config: UseFrappeCollectionConfig | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Story and spec injection. Production callers omit this; the live socket
|
|
16
|
+
* from `useFrappeConnection(config)` wins.
|
|
17
|
+
*/
|
|
18
|
+
connection?: FrappeConnectionState;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
/**
|
|
@@ -19,10 +28,11 @@ export interface FrappeReconnectIndicatorProps {
|
|
|
19
28
|
* gap is owned by the client's reconnect backfill; this chip only tells the
|
|
20
29
|
* user that in-place updates are paused right now.
|
|
21
30
|
*/
|
|
22
|
-
export function FrappeReconnectIndicator({ config }: FrappeReconnectIndicatorProps) {
|
|
31
|
+
export function FrappeReconnectIndicator({ config, connection }: FrappeReconnectIndicatorProps) {
|
|
23
32
|
const { t } = useTranslation();
|
|
24
33
|
const { knobProps } = useResolvedKnobs();
|
|
25
|
-
const
|
|
34
|
+
const hooked = useFrappeConnection(config);
|
|
35
|
+
const { connected, everConnected } = connection ?? hooked;
|
|
26
36
|
if (!everConnected || connected) return null;
|
|
27
37
|
return (
|
|
28
38
|
<XStack
|