@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.
Files changed (114) hide show
  1. package/dist/cjs/FileBrowser/AttachLibrary.cjs +51 -49
  2. package/dist/cjs/FileBrowser/AttachLibrary.native.js +58 -49
  3. package/dist/cjs/FileBrowser/AttachLibrary.native.js.map +1 -1
  4. package/dist/cjs/FrappeConnectionIndicator.cjs +5 -3
  5. package/dist/cjs/FrappeConnectionIndicator.native.js +5 -3
  6. package/dist/cjs/FrappeConnectionIndicator.native.js.map +1 -1
  7. package/dist/cjs/FrappeForm.cjs +3 -1
  8. package/dist/cjs/FrappeForm.native.js +3 -1
  9. package/dist/cjs/FrappeForm.native.js.map +1 -1
  10. package/dist/cjs/FrappeReconnectIndicator.cjs +4 -2
  11. package/dist/cjs/FrappeReconnectIndicator.native.js +4 -2
  12. package/dist/cjs/FrappeReconnectIndicator.native.js.map +1 -1
  13. package/dist/cjs/index.cjs +2 -0
  14. package/dist/cjs/index.native.js +2 -0
  15. package/dist/cjs/index.native.js.map +1 -1
  16. package/dist/cjs/schemaForm/applyFieldOrder.cjs +63 -0
  17. package/dist/cjs/schemaForm/applyFieldOrder.native.js +116 -0
  18. package/dist/cjs/schemaForm/applyFieldOrder.native.js.map +1 -0
  19. package/dist/cjs/schemaForm/index.cjs +13 -7
  20. package/dist/cjs/schemaForm/index.native.js +15 -7
  21. package/dist/cjs/schemaForm/index.native.js.map +1 -1
  22. package/dist/esm/FileBrowser/AttachLibrary.mjs +51 -49
  23. package/dist/esm/FileBrowser/AttachLibrary.mjs.map +1 -1
  24. package/dist/esm/FileBrowser/AttachLibrary.native.js +58 -49
  25. package/dist/esm/FileBrowser/AttachLibrary.native.js.map +1 -1
  26. package/dist/esm/FrappeConnectionIndicator.mjs +5 -3
  27. package/dist/esm/FrappeConnectionIndicator.mjs.map +1 -1
  28. package/dist/esm/FrappeConnectionIndicator.native.js +5 -3
  29. package/dist/esm/FrappeConnectionIndicator.native.js.map +1 -1
  30. package/dist/esm/FrappeForm.mjs +3 -1
  31. package/dist/esm/FrappeForm.mjs.map +1 -1
  32. package/dist/esm/FrappeForm.native.js +3 -1
  33. package/dist/esm/FrappeForm.native.js.map +1 -1
  34. package/dist/esm/FrappeReconnectIndicator.mjs +4 -2
  35. package/dist/esm/FrappeReconnectIndicator.mjs.map +1 -1
  36. package/dist/esm/FrappeReconnectIndicator.native.js +4 -2
  37. package/dist/esm/FrappeReconnectIndicator.native.js.map +1 -1
  38. package/dist/esm/index.mjs +2 -2
  39. package/dist/esm/index.mjs.map +1 -1
  40. package/dist/esm/index.native.js +2 -2
  41. package/dist/esm/index.native.js.map +1 -1
  42. package/dist/esm/schemaForm/applyFieldOrder.mjs +37 -0
  43. package/dist/esm/schemaForm/applyFieldOrder.mjs.map +1 -0
  44. package/dist/esm/schemaForm/applyFieldOrder.native.js +87 -0
  45. package/dist/esm/schemaForm/applyFieldOrder.native.js.map +1 -0
  46. package/dist/esm/schemaForm/index.mjs +11 -7
  47. package/dist/esm/schemaForm/index.mjs.map +1 -1
  48. package/dist/esm/schemaForm/index.native.js +13 -7
  49. package/dist/esm/schemaForm/index.native.js.map +1 -1
  50. package/dist/jsx/FileBrowser/AttachLibrary.mjs +51 -49
  51. package/dist/jsx/FileBrowser/AttachLibrary.mjs.map +1 -1
  52. package/dist/jsx/FileBrowser/AttachLibrary.native.js +58 -49
  53. package/dist/jsx/FileBrowser/AttachLibrary.native.js.map +1 -1
  54. package/dist/jsx/FrappeConnectionIndicator.mjs +5 -3
  55. package/dist/jsx/FrappeConnectionIndicator.mjs.map +1 -1
  56. package/dist/jsx/FrappeConnectionIndicator.native.js +5 -3
  57. package/dist/jsx/FrappeConnectionIndicator.native.js.map +1 -1
  58. package/dist/jsx/FrappeForm.mjs +3 -1
  59. package/dist/jsx/FrappeForm.mjs.map +1 -1
  60. package/dist/jsx/FrappeForm.native.js +3 -1
  61. package/dist/jsx/FrappeForm.native.js.map +1 -1
  62. package/dist/jsx/FrappeReconnectIndicator.mjs +4 -2
  63. package/dist/jsx/FrappeReconnectIndicator.mjs.map +1 -1
  64. package/dist/jsx/FrappeReconnectIndicator.native.js +4 -2
  65. package/dist/jsx/FrappeReconnectIndicator.native.js.map +1 -1
  66. package/dist/jsx/index.js +2 -2
  67. package/dist/jsx/index.js.map +1 -1
  68. package/dist/jsx/index.mjs +2 -2
  69. package/dist/jsx/index.mjs.map +1 -1
  70. package/dist/jsx/index.native.js +2 -0
  71. package/dist/jsx/index.native.js.map +1 -1
  72. package/dist/jsx/schemaForm/applyFieldOrder.mjs +37 -0
  73. package/dist/jsx/schemaForm/applyFieldOrder.mjs.map +1 -0
  74. package/dist/jsx/schemaForm/applyFieldOrder.native.js +116 -0
  75. package/dist/jsx/schemaForm/applyFieldOrder.native.js.map +1 -0
  76. package/dist/jsx/schemaForm/index.mjs +11 -7
  77. package/dist/jsx/schemaForm/index.mjs.map +1 -1
  78. package/dist/jsx/schemaForm/index.native.js +15 -7
  79. package/dist/jsx/schemaForm/index.native.js.map +1 -1
  80. package/package.json +16 -16
  81. package/src/FileBrowser/AttachLibrary.spec.tsx +74 -0
  82. package/src/FileBrowser/AttachLibrary.stories.tsx +67 -0
  83. package/src/FileBrowser/AttachLibrary.tsx +56 -49
  84. package/src/FrappeConnectionIndicator.spec.tsx +51 -0
  85. package/src/FrappeConnectionIndicator.stories.tsx +55 -0
  86. package/src/FrappeConnectionIndicator.tsx +15 -4
  87. package/src/FrappeForm.tsx +13 -3
  88. package/src/FrappeReconnectIndicator.spec.tsx +52 -0
  89. package/src/FrappeReconnectIndicator.stories.tsx +55 -0
  90. package/src/FrappeReconnectIndicator.tsx +13 -3
  91. package/src/SyncErrorNotice.stories.tsx +50 -0
  92. package/src/desk/DeskFormSidebar.spec.tsx +90 -0
  93. package/src/desk/DeskFormSidebar.stories.tsx +133 -0
  94. package/src/fields/LinkTree.spec.tsx +109 -0
  95. package/src/fields/LinkTree.stories.tsx +187 -0
  96. package/src/index.ts +1 -1
  97. package/src/schemaForm/SchemaForm.spec.tsx +29 -0
  98. package/src/schemaForm/SchemaForm.stories.tsx +14 -1
  99. package/src/schemaForm/applyFieldOrder.spec.ts +75 -0
  100. package/src/schemaForm/applyFieldOrder.ts +71 -0
  101. package/src/schemaForm/index.tsx +17 -5
  102. package/types/FileBrowser/AttachLibrary.d.ts.map +1 -1
  103. package/types/FrappeConnectionIndicator.d.ts +8 -2
  104. package/types/FrappeConnectionIndicator.d.ts.map +1 -1
  105. package/types/FrappeForm.d.ts +7 -1
  106. package/types/FrappeForm.d.ts.map +1 -1
  107. package/types/FrappeReconnectIndicator.d.ts +7 -2
  108. package/types/FrappeReconnectIndicator.d.ts.map +1 -1
  109. package/types/index.d.ts +1 -1
  110. package/types/index.d.ts.map +1 -1
  111. package/types/schemaForm/applyFieldOrder.d.ts +28 -0
  112. package/types/schemaForm/applyFieldOrder.d.ts.map +1 -0
  113. package/types/schemaForm/index.d.ts +8 -1
  114. 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.1",
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.1",
82
- "@multiplatform.one/desktop": "7.6.1",
83
- "@multiplatform.one/forms": "7.6.1",
84
- "@multiplatform.one/frappe": "7.6.1",
85
- "@multiplatform.one/i18n": "7.6.1",
86
- "@multiplatform.one/markdown": "7.6.1",
87
- "@multiplatform.one/platform": "7.6.1",
88
- "@multiplatform.one/rich-text": "7.6.1",
89
- "@multiplatform.one/router": "7.6.1",
90
- "@multiplatform.one/store": "7.6.1",
91
- "@multiplatform.one/table": "7.6.1",
92
- "@multiplatform.one/theme": "7.6.1"
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.1",
106
- "@multiplatform.one/storybook": "7.6.1",
107
- "@multiplatform.one/test-utils": "7.6.1"
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
- <DialogOverlay backgroundColor="$background" opacity={0.5} data-filebrowser-attach-scrim />
40
- <DialogContent
41
- aria-label={t("Attach file")}
42
- data-filebrowser-attach-dialog
43
- width={drill ? "100%" : 820}
44
- maxWidth="100%"
45
- >
46
- <Text fontSize={20} lineHeight={32} fontFamily="$mono">
47
- {t("Attach file")}
48
- </Text>
49
- <Tabs
50
- variant="contained"
51
- value={tab}
52
- onChange={setTab}
53
- items={[
54
- {
55
- value: "upload",
56
- label: t("Upload"),
57
- content: (
58
- <YStack paddingTop="$4">
59
- <FileUpload accept={accept} />
60
- </YStack>
61
- ),
62
- },
63
- {
64
- value: "library",
65
- label: t("From library"),
66
- content: (
67
- <YStack paddingTop="$4" data-filebrowser-attach-library>
68
- <FileBrowser
69
- files={files}
70
- layout={drill ? "drill" : "split"}
71
- selectionMode="single"
72
- currentValue={current}
73
- currentFolder={currentFolder}
74
- surfaceId="attach-library"
75
- onPick={(file) => {
76
- onPick(file);
77
- onOpenChange(false);
78
- }}
79
- accept={accept}
80
- onUpload={onUpload}
81
- />
82
- </YStack>
83
- ),
84
- },
85
- ]}
86
- />
87
- </DialogContent>
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 { useFrappeConnection, type UseFrappeCollectionConfig } from "@multiplatform.one/frappe";
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 { connected, everConnected } = useFrappeConnection(config);
34
+ const hooked = useFrappeConnection(config);
35
+ const { connected, everConnected } = connection ?? hooked;
25
36
  if (connected || !everConnected) return null;
26
37
  return (
27
38
  <XStack
@@ -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((col): GenericFormColumn => ({
121
- fields: col.fields.map(mapFieldToGenericSchema),
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 { useFrappeConnection, type UseFrappeCollectionConfig } from "@multiplatform.one/frappe";
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 { connected, everConnected } = useFrappeConnection(config);
34
+ const hooked = useFrappeConnection(config);
35
+ const { connected, everConnected } = connection ?? hooked;
26
36
  if (!everConnected || connected) return null;
27
37
  return (
28
38
  <XStack