@multiplatform.one/frappe-ui 7.11.0 → 7.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cjs/FrappeDashboard.cjs +8 -10
- package/dist/cjs/FrappeDashboard.native.js +8 -14
- package/dist/cjs/FrappeDashboard.native.js.map +1 -1
- package/dist/cjs/FrappeForm.cjs +465 -175
- package/dist/cjs/FrappeForm.native.js +611 -207
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeFormConnections.cjs +125 -0
- package/dist/cjs/FrappeFormConnections.native.js +149 -0
- package/dist/cjs/FrappeFormConnections.native.js.map +1 -0
- package/dist/cjs/FrappeImageView.cjs +7 -9
- package/dist/cjs/FrappeImageView.native.js +7 -13
- package/dist/cjs/FrappeImageView.native.js.map +1 -1
- package/dist/cjs/FrappeList.cjs +29 -4
- package/dist/cjs/FrappeList.native.js +37 -5
- package/dist/cjs/FrappeList.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +102 -29
- package/dist/cjs/FrappeListView.native.js +109 -29
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeProvider.cjs +7 -2
- package/dist/cjs/FrappeProvider.native.js +7 -2
- package/dist/cjs/FrappeProvider.native.js.map +1 -1
- package/dist/cjs/FrappeReportBuilder.cjs +10 -11
- package/dist/cjs/FrappeReportBuilder.native.js +10 -16
- package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
- package/dist/cjs/FrappeRowsReader.cjs +49 -0
- package/dist/cjs/FrappeRowsReader.native.js +53 -0
- package/dist/cjs/FrappeRowsReader.native.js.map +1 -0
- package/dist/cjs/FrappeTable.cjs +18 -6
- package/dist/cjs/FrappeTable.native.js +24 -6
- package/dist/cjs/FrappeTable.native.js.map +1 -1
- package/dist/cjs/fieldRegistry.cjs +36 -11
- package/dist/cjs/fieldRegistry.native.js +59 -11
- package/dist/cjs/fieldRegistry.native.js.map +1 -1
- package/dist/cjs/fields/Date/index.native.js +5 -2
- package/dist/cjs/fields/Date/index.native.js.map +1 -1
- package/dist/cjs/fields/Date/sheetKeyboard.cjs +2 -2
- package/dist/cjs/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/cjs/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/cjs/fields/DateRange/index.cjs +2 -1
- package/dist/cjs/fields/DateRange/index.native.js +4 -1
- package/dist/cjs/fields/DateRange/index.native.js.map +1 -1
- package/dist/cjs/formEngine/index.cjs +4 -2
- package/dist/cjs/formEngine/index.native.js +3 -1
- package/dist/cjs/formEngine/index.native.js.map +1 -1
- package/dist/cjs/formEngine/useSessionRoles.cjs +77 -0
- package/dist/cjs/formEngine/useSessionRoles.native.js +84 -0
- package/dist/cjs/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/cjs/frappeConnections.cjs +41 -0
- package/dist/cjs/frappeConnections.native.js +50 -0
- package/dist/cjs/frappeConnections.native.js.map +1 -0
- package/dist/cjs/frappeNavigation.cjs +107 -0
- package/dist/cjs/frappeNavigation.native.js +112 -0
- package/dist/cjs/frappeNavigation.native.js.map +1 -0
- package/dist/cjs/index.cjs +18 -1
- package/dist/cjs/index.native.js +18 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/schemaForm/index.cjs +4 -1
- package/dist/cjs/schemaForm/index.native.js +4 -1
- package/dist/cjs/schemaForm/index.native.js.map +1 -1
- package/dist/cjs/useConnectionFilters.cjs +146 -0
- package/dist/cjs/useConnectionFilters.native.js +213 -0
- package/dist/cjs/useConnectionFilters.native.js.map +1 -0
- package/dist/cjs/useFrappePagination.cjs +37 -18
- package/dist/cjs/useFrappePagination.native.js +59 -23
- package/dist/cjs/useFrappePagination.native.js.map +1 -1
- package/dist/cjs/useFrappeRows.native.js.map +1 -1
- package/dist/esm/FrappeDashboard.mjs +9 -11
- package/dist/esm/FrappeDashboard.mjs.map +1 -1
- package/dist/esm/FrappeDashboard.native.js +9 -15
- package/dist/esm/FrappeDashboard.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +471 -181
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +599 -195
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeFormConnections.mjs +99 -0
- package/dist/esm/FrappeFormConnections.mjs.map +1 -0
- package/dist/esm/FrappeFormConnections.native.js +120 -0
- package/dist/esm/FrappeFormConnections.native.js.map +1 -0
- package/dist/esm/FrappeImageView.mjs +8 -10
- package/dist/esm/FrappeImageView.mjs.map +1 -1
- package/dist/esm/FrappeImageView.native.js +8 -14
- package/dist/esm/FrappeImageView.native.js.map +1 -1
- package/dist/esm/FrappeList.mjs +31 -6
- package/dist/esm/FrappeList.mjs.map +1 -1
- package/dist/esm/FrappeList.native.js +39 -7
- package/dist/esm/FrappeList.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +103 -30
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +110 -30
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/FrappeProvider.mjs +7 -2
- package/dist/esm/FrappeProvider.mjs.map +1 -1
- package/dist/esm/FrappeProvider.native.js +7 -2
- package/dist/esm/FrappeProvider.native.js.map +1 -1
- package/dist/esm/FrappeReportBuilder.mjs +11 -12
- package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
- package/dist/esm/FrappeReportBuilder.native.js +11 -17
- package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
- package/dist/esm/FrappeRowsReader.mjs +24 -0
- package/dist/esm/FrappeRowsReader.mjs.map +1 -0
- package/dist/esm/FrappeRowsReader.native.js +25 -0
- package/dist/esm/FrappeRowsReader.native.js.map +1 -0
- package/dist/esm/FrappeTable.mjs +18 -6
- package/dist/esm/FrappeTable.mjs.map +1 -1
- package/dist/esm/FrappeTable.native.js +24 -6
- package/dist/esm/FrappeTable.native.js.map +1 -1
- package/dist/esm/fieldRegistry.mjs +35 -12
- package/dist/esm/fieldRegistry.mjs.map +1 -1
- package/dist/esm/fieldRegistry.native.js +58 -12
- package/dist/esm/fieldRegistry.native.js.map +1 -1
- package/dist/esm/fields/Date/index.native.js +6 -3
- package/dist/esm/fields/Date/index.native.js.map +1 -1
- package/dist/esm/fields/Date/sheetKeyboard.mjs +2 -2
- package/dist/esm/fields/Date/sheetKeyboard.mjs.map +1 -1
- package/dist/esm/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/esm/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/esm/fields/DateRange/index.mjs +2 -1
- package/dist/esm/fields/DateRange/index.mjs.map +1 -1
- package/dist/esm/fields/DateRange/index.native.js +4 -1
- package/dist/esm/fields/DateRange/index.native.js.map +1 -1
- package/dist/esm/formEngine/index.mjs +2 -1
- package/dist/esm/formEngine/index.mjs.map +1 -1
- package/dist/esm/formEngine/index.native.js +2 -1
- package/dist/esm/formEngine/index.native.js.map +1 -1
- package/dist/esm/formEngine/useSessionRoles.mjs +52 -0
- package/dist/esm/formEngine/useSessionRoles.mjs.map +1 -0
- package/dist/esm/formEngine/useSessionRoles.native.js +56 -0
- package/dist/esm/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/esm/frappeConnections.mjs +15 -0
- package/dist/esm/frappeConnections.mjs.map +1 -0
- package/dist/esm/frappeConnections.native.js +21 -0
- package/dist/esm/frappeConnections.native.js.map +1 -0
- package/dist/esm/frappeNavigation.mjs +78 -0
- package/dist/esm/frappeNavigation.mjs.map +1 -0
- package/dist/esm/frappeNavigation.native.js +80 -0
- package/dist/esm/frappeNavigation.native.js.map +1 -0
- package/dist/esm/index.mjs +7 -3
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +7 -3
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/schemaForm/index.mjs +4 -1
- package/dist/esm/schemaForm/index.mjs.map +1 -1
- package/dist/esm/schemaForm/index.native.js +4 -1
- package/dist/esm/schemaForm/index.native.js.map +1 -1
- package/dist/esm/useConnectionFilters.mjs +121 -0
- package/dist/esm/useConnectionFilters.mjs.map +1 -0
- package/dist/esm/useConnectionFilters.native.js +185 -0
- package/dist/esm/useConnectionFilters.native.js.map +1 -0
- package/dist/esm/useFrappePagination.mjs +38 -19
- package/dist/esm/useFrappePagination.mjs.map +1 -1
- package/dist/esm/useFrappePagination.native.js +60 -24
- package/dist/esm/useFrappePagination.native.js.map +1 -1
- package/dist/esm/useFrappeRows.mjs.map +1 -1
- package/dist/esm/useFrappeRows.native.js.map +1 -1
- package/dist/jsx/FrappeDashboard.mjs +9 -11
- package/dist/jsx/FrappeDashboard.mjs.map +1 -1
- package/dist/jsx/FrappeDashboard.native.js +8 -14
- package/dist/jsx/FrappeDashboard.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +471 -181
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +611 -207
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeFormConnections.mjs +99 -0
- package/dist/jsx/FrappeFormConnections.mjs.map +1 -0
- package/dist/jsx/FrappeFormConnections.native.js +149 -0
- package/dist/jsx/FrappeFormConnections.native.js.map +1 -0
- package/dist/jsx/FrappeImageView.mjs +8 -10
- package/dist/jsx/FrappeImageView.mjs.map +1 -1
- package/dist/jsx/FrappeImageView.native.js +7 -13
- package/dist/jsx/FrappeImageView.native.js.map +1 -1
- package/dist/jsx/FrappeList.mjs +31 -6
- package/dist/jsx/FrappeList.mjs.map +1 -1
- package/dist/jsx/FrappeList.native.js +37 -5
- package/dist/jsx/FrappeList.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +103 -30
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +109 -29
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/FrappeProvider.mjs +7 -2
- package/dist/jsx/FrappeProvider.mjs.map +1 -1
- package/dist/jsx/FrappeProvider.native.js +7 -2
- package/dist/jsx/FrappeProvider.native.js.map +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs +11 -12
- package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
- package/dist/jsx/FrappeReportBuilder.native.js +10 -16
- package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
- package/dist/jsx/FrappeRowsReader.mjs +24 -0
- package/dist/jsx/FrappeRowsReader.mjs.map +1 -0
- package/dist/jsx/FrappeRowsReader.native.js +53 -0
- package/dist/jsx/FrappeRowsReader.native.js.map +1 -0
- package/dist/jsx/FrappeTable.mjs +18 -6
- package/dist/jsx/FrappeTable.mjs.map +1 -1
- package/dist/jsx/FrappeTable.native.js +24 -6
- package/dist/jsx/FrappeTable.native.js.map +1 -1
- package/dist/jsx/fieldRegistry.mjs +35 -12
- package/dist/jsx/fieldRegistry.mjs.map +1 -1
- package/dist/jsx/fieldRegistry.native.js +59 -11
- package/dist/jsx/fieldRegistry.native.js.map +1 -1
- package/dist/jsx/fields/Date/index.native.js +5 -2
- package/dist/jsx/fields/Date/index.native.js.map +1 -1
- package/dist/jsx/fields/Date/sheetKeyboard.mjs +2 -2
- package/dist/jsx/fields/Date/sheetKeyboard.mjs.map +1 -1
- package/dist/jsx/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/jsx/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/jsx/fields/DateRange/index.mjs +2 -1
- package/dist/jsx/fields/DateRange/index.mjs.map +1 -1
- package/dist/jsx/fields/DateRange/index.native.js +4 -1
- package/dist/jsx/fields/DateRange/index.native.js.map +1 -1
- package/dist/jsx/formEngine/index.mjs +2 -1
- package/dist/jsx/formEngine/index.mjs.map +1 -1
- package/dist/jsx/formEngine/index.native.js +3 -1
- package/dist/jsx/formEngine/index.native.js.map +1 -1
- package/dist/jsx/formEngine/useSessionRoles.mjs +52 -0
- package/dist/jsx/formEngine/useSessionRoles.mjs.map +1 -0
- package/dist/jsx/formEngine/useSessionRoles.native.js +84 -0
- package/dist/jsx/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/jsx/frappeConnections.mjs +15 -0
- package/dist/jsx/frappeConnections.mjs.map +1 -0
- package/dist/jsx/frappeConnections.native.js +50 -0
- package/dist/jsx/frappeConnections.native.js.map +1 -0
- package/dist/jsx/frappeNavigation.mjs +78 -0
- package/dist/jsx/frappeNavigation.mjs.map +1 -0
- package/dist/jsx/frappeNavigation.native.js +112 -0
- package/dist/jsx/frappeNavigation.native.js.map +1 -0
- package/dist/jsx/index.js +7 -3
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +7 -3
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +18 -1
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/schemaForm/index.mjs +4 -1
- package/dist/jsx/schemaForm/index.mjs.map +1 -1
- package/dist/jsx/schemaForm/index.native.js +4 -1
- package/dist/jsx/schemaForm/index.native.js.map +1 -1
- package/dist/jsx/useConnectionFilters.mjs +121 -0
- package/dist/jsx/useConnectionFilters.mjs.map +1 -0
- package/dist/jsx/useConnectionFilters.native.js +213 -0
- package/dist/jsx/useConnectionFilters.native.js.map +1 -0
- package/dist/jsx/useFrappePagination.mjs +38 -19
- package/dist/jsx/useFrappePagination.mjs.map +1 -1
- package/dist/jsx/useFrappePagination.native.js +59 -23
- package/dist/jsx/useFrappePagination.native.js.map +1 -1
- package/dist/jsx/useFrappeRows.mjs.map +1 -1
- package/dist/jsx/useFrappeRows.native.js.map +1 -1
- package/package.json +24 -20
- package/src/FileBrowser/AttachLibrary.stories.tsx +23 -0
- package/src/FrappeDashboard.tsx +11 -16
- package/src/FrappeForm.connections.spec.tsx +280 -0
- package/src/FrappeForm.connections.stories.tsx +93 -0
- package/src/FrappeForm.deskSemantics.spec.tsx +284 -0
- package/src/FrappeForm.tsx +560 -145
- package/src/FrappeFormConnections.tsx +148 -0
- package/src/FrappeGuestMeta.spec.tsx +143 -0
- package/src/FrappeImageView.tsx +8 -12
- package/src/FrappeList.tsx +71 -15
- package/src/FrappeListView.cards.spec.tsx +175 -0
- package/src/FrappeListView.cards.stories.tsx +131 -0
- package/src/FrappeListView.connections.spec.tsx +192 -0
- package/src/FrappeListView.connections.stories.tsx +99 -0
- package/src/FrappeListView.seed.spec.tsx +228 -0
- package/src/FrappeListView.tsx +177 -31
- package/src/FrappeProvider.tsx +13 -1
- package/src/FrappeReportBuilder.tsx +11 -19
- package/src/FrappeRowsReader.tsx +38 -0
- package/src/FrappeTable.optionalProps.spec.tsx +249 -0
- package/src/FrappeTable.spec.tsx +32 -42
- package/src/FrappeTable.stories.spec.tsx +55 -0
- package/src/FrappeTable.stories.tsx +28 -37
- package/src/FrappeTable.tsx +108 -45
- package/src/childTableDependsOn.spec.tsx +85 -0
- package/src/desk/DeskFormSidebar.spec.tsx +20 -0
- package/src/desk/DeskFormSidebar.stories.tsx +42 -0
- package/src/fieldRegistry.spec.tsx +27 -64
- package/src/fieldRegistry.ts +71 -14
- package/src/fields/Date/index.native.tsx +4 -2
- package/src/fields/Date/sheetKeyboard.spec.ts +14 -5
- package/src/fields/Date/sheetKeyboard.ts +12 -4
- package/src/fields/DateRange/DateRange.spec.tsx +67 -0
- package/src/fields/DateRange/index.tsx +5 -1
- package/src/fields/LinkTree.spec.tsx +8 -0
- package/src/fields/LinkTree.stories.tsx +28 -5
- package/src/fields/Select/Select.hydration.spec.tsx +177 -0
- package/src/formEngine/doctypeMeta.ts +15 -114
- package/src/formEngine/index.ts +2 -0
- package/src/formEngine/seam.spec.ts +23 -4
- package/src/formEngine/useSessionRoles.ts +73 -0
- package/src/frappeConnections.ts +50 -0
- package/src/frappeNavigation.tsx +112 -0
- package/src/frappeRoutes.spec.tsx +335 -0
- package/src/index.ts +28 -1
- package/src/schemaForm/index.tsx +1 -0
- package/src/useConnectionFilters.tsx +157 -0
- package/src/useFrappePagination.ts +70 -26
- package/src/useFrappeRows.sharedPath.spec.tsx +206 -0
- package/src/useFrappeRows.ts +5 -4
- package/types/FrappeDashboard.d.ts +3 -4
- package/types/FrappeDashboard.d.ts.map +1 -1
- package/types/FrappeForm.d.ts +57 -5
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeFormConnections.d.ts +18 -0
- package/types/FrappeFormConnections.d.ts.map +1 -0
- package/types/FrappeImageView.d.ts +2 -3
- package/types/FrappeImageView.d.ts.map +1 -1
- package/types/FrappeList.d.ts +20 -2
- package/types/FrappeList.d.ts.map +1 -1
- package/types/FrappeListView.d.ts +53 -3
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/FrappeProvider.d.ts +8 -1
- package/types/FrappeProvider.d.ts.map +1 -1
- package/types/FrappeReportBuilder.d.ts.map +1 -1
- package/types/FrappeRowsReader.d.ts +26 -0
- package/types/FrappeRowsReader.d.ts.map +1 -0
- package/types/FrappeTable.d.ts +39 -8
- package/types/FrappeTable.d.ts.map +1 -1
- package/types/fieldRegistry.d.ts +17 -3
- package/types/fieldRegistry.d.ts.map +1 -1
- package/types/fields/Date/index.native.d.ts.map +1 -1
- package/types/fields/Date/sheetKeyboard.d.ts +4 -3
- package/types/fields/Date/sheetKeyboard.d.ts.map +1 -1
- package/types/fields/DateRange/index.d.ts.map +1 -1
- package/types/formEngine/doctypeMeta.d.ts +10 -114
- package/types/formEngine/doctypeMeta.d.ts.map +1 -1
- package/types/formEngine/index.d.ts +1 -0
- package/types/formEngine/index.d.ts.map +1 -1
- package/types/formEngine/useSessionRoles.d.ts +19 -0
- package/types/formEngine/useSessionRoles.d.ts.map +1 -0
- package/types/frappeConnections.d.ts +33 -0
- package/types/frappeConnections.d.ts.map +1 -0
- package/types/frappeNavigation.d.ts +49 -0
- package/types/frappeNavigation.d.ts.map +1 -0
- package/types/index.d.ts +7 -3
- package/types/index.d.ts.map +1 -1
- package/types/schemaForm/index.d.ts.map +1 -1
- package/types/useConnectionFilters.d.ts +34 -0
- package/types/useConnectionFilters.d.ts.map +1 -0
- package/types/useFrappePagination.d.ts +18 -5
- package/types/useFrappePagination.d.ts.map +1 -1
- package/types/useFrappeRows.d.ts +5 -4
- package/types/useFrappeRows.d.ts.map +1 -1
- package/src/FileBrowser/AttachLibrary.spec.tsx +0 -74
package/src/FrappeForm.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
getFrappeSyncModule,
|
|
2
3
|
useFrappeCollection,
|
|
3
4
|
useFrappeDoctypeEvents,
|
|
4
5
|
useLiveQuery,
|
|
@@ -7,32 +8,54 @@ import {
|
|
|
7
8
|
type DocTypeField,
|
|
8
9
|
type FormSection,
|
|
9
10
|
} from "@multiplatform.one/frappe";
|
|
10
|
-
import { AsyncSkeleton } from "@multiplatform.one/components";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
AsyncBoundary,
|
|
13
|
+
AsyncSkeleton,
|
|
14
|
+
DetailPageLayout,
|
|
15
|
+
PageHeader,
|
|
16
|
+
Tabs,
|
|
17
|
+
type TabsItem,
|
|
18
|
+
} from "@multiplatform.one/components";
|
|
19
|
+
import { SchemaForm, type FieldOverride } from "./schemaForm";
|
|
18
20
|
import type { HeadingLevel } from "./fields/Heading";
|
|
19
|
-
import type { FieldSchema } from "./fieldRegistry";
|
|
20
21
|
import { FormSidebar, type SidebarSection, type SidebarMetadataEntry } from "./FormSidebar";
|
|
21
22
|
import { eq } from "@tanstack/db";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
ArrowLeftIcon,
|
|
25
|
+
FloppyDiskIcon,
|
|
26
|
+
PencilSimpleIcon,
|
|
27
|
+
TrashIcon,
|
|
28
|
+
XIcon,
|
|
29
|
+
} from "@phosphor-icons/react";
|
|
23
30
|
import type React from "react";
|
|
24
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
31
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
25
32
|
import type { TFunction } from "i18next";
|
|
26
33
|
import { useTranslation } from "react-i18next";
|
|
27
|
-
import { Button, formCommonColors } from "@multiplatform.one/forms";
|
|
34
|
+
import { Button, Input, formCommonColors } from "@multiplatform.one/forms";
|
|
28
35
|
import { Text, XStack, YStack } from "tamagui";
|
|
29
36
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
30
37
|
import { useFrappeConfig } from "./FrappeProvider";
|
|
31
38
|
import { FrappeReconnectIndicator } from "./FrappeReconnectIndicator";
|
|
32
39
|
import { Timeline } from "./components/timeline";
|
|
33
40
|
import { dialog } from "@multiplatform.one/desktop";
|
|
41
|
+
import { applyFetchedValues, buildFetchMap } from "./formEngine/fetchFrom";
|
|
42
|
+
import { buildPermLevelMatrix } from "./formEngine/fieldPermissions";
|
|
43
|
+
import { createHttpClient, validateLink } from "./formEngine/formActions";
|
|
44
|
+
import { buildFormTabs, isPromptNamed, type FormTab } from "./formEngine/formLayout";
|
|
45
|
+
import { useSessionRoles } from "./formEngine/useSessionRoles";
|
|
46
|
+
import { connectionFields, connectionsFromMeta, type FrappeConnection } from "./frappeConnections";
|
|
47
|
+
import { FrappeFormConnections } from "./FrappeFormConnections";
|
|
48
|
+
import { FrappeRowsReader, type FrappeRowsSnapshot } from "./FrappeRowsReader";
|
|
49
|
+
import {
|
|
50
|
+
frappeRouteHref,
|
|
51
|
+
frappeRouteTarget,
|
|
52
|
+
useFrappeRouter,
|
|
53
|
+
type FrappeRouteTarget,
|
|
54
|
+
} from "./frappeNavigation";
|
|
55
|
+
import type { FieldSchema } from "./fieldRegistry";
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
/** Linked rows per connection doctype, as the render prop receives them. */
|
|
58
|
+
export type FrappeConnectionRows = Record<string, Array<Record<string, unknown>>>;
|
|
36
59
|
|
|
37
60
|
function isEmptyFieldValue(value: unknown): boolean {
|
|
38
61
|
if (value === undefined || value === null) return true;
|
|
@@ -57,7 +80,32 @@ export interface FrappeFormProps<TDoc extends { name: string; doctype: string }>
|
|
|
57
80
|
meta?: DocTypeMeta;
|
|
58
81
|
/** True when the form is read-only (disabled prop, or doc deleted on the server). */
|
|
59
82
|
disabled: boolean;
|
|
83
|
+
/** The record's linked rows per connection doctype (edit mode), live. */
|
|
84
|
+
connections: FrappeConnectionRows;
|
|
85
|
+
/** A connection's rows are still being read. */
|
|
86
|
+
connectionsLoading: boolean;
|
|
60
87
|
}) => React.ReactNode;
|
|
88
|
+
/**
|
|
89
|
+
* Doctypes linking back to this one (the desk's Connections). Defaults to
|
|
90
|
+
* meta.links; declare them where the reader cannot load meta (Guest).
|
|
91
|
+
* Schema mode shows them on a Connections tab; render-prop mode hands the
|
|
92
|
+
* rows to `children`.
|
|
93
|
+
*/
|
|
94
|
+
connections?: readonly FrappeConnection[];
|
|
95
|
+
/** False leaves the Connections tab out of schema mode. */
|
|
96
|
+
showConnections?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The document a route loader rendered on the server (`loadDoc`), shown
|
|
99
|
+
* until the live read lands so the server page and the hydrated page match.
|
|
100
|
+
* Ignored unless its name is `name`.
|
|
101
|
+
*/
|
|
102
|
+
initialDoc?: Partial<TDoc>;
|
|
103
|
+
/** Replaces the skeleton while the document loads. */
|
|
104
|
+
loadingFallback?: React.ReactNode;
|
|
105
|
+
/** Replaces the not-found message for a name that does not resolve. */
|
|
106
|
+
notFoundFallback?: React.ReactNode;
|
|
107
|
+
/** Replaces the error state when the document cannot be read. */
|
|
108
|
+
errorFallback?: React.ReactNode;
|
|
61
109
|
/** Per-field overrides for schema-driven mode (when children not provided). */
|
|
62
110
|
overrides?: Record<string, FieldOverride>;
|
|
63
111
|
/** Fields to hide in schema-driven mode. */
|
|
@@ -72,9 +120,31 @@ export interface FrappeFormProps<TDoc extends { name: string; doctype: string }>
|
|
|
72
120
|
* names are ignored. Cheaper than replacing `sections`.
|
|
73
121
|
*/
|
|
74
122
|
order?: string[];
|
|
123
|
+
/**
|
|
124
|
+
* Session roles for permlevel resolution. Omitted: the form asks the site
|
|
125
|
+
* for them when the doctype has fields above permlevel 0, and a fixtures
|
|
126
|
+
* connection takes the conservative rule (permlevel > 0 is read-only,
|
|
127
|
+
* never hidden).
|
|
128
|
+
*/
|
|
129
|
+
roles?: string[];
|
|
130
|
+
/** Called after a save; a created document carries the name the server gave it. */
|
|
75
131
|
onSave?: (doc: Partial<TDoc>) => void;
|
|
76
132
|
onCancel?: () => void;
|
|
77
133
|
onDelete?: (doc: Partial<TDoc>) => void;
|
|
134
|
+
/**
|
|
135
|
+
* Given, the form is a read-only view of the record: its toolbar offers
|
|
136
|
+
* Edit (this callback) and Delete instead of Save and Cancel.
|
|
137
|
+
*/
|
|
138
|
+
onEdit?: () => void;
|
|
139
|
+
/**
|
|
140
|
+
* The path the doctype's pages live under (`/moves`). Given, the form is
|
|
141
|
+
* the doctype's record page: it reads the record from the path through
|
|
142
|
+
* the FrappeProvider's navigation (`route/new` creates, `route/<name>`
|
|
143
|
+
* views, `route/<name>/edit` edits), carries the detail page template,
|
|
144
|
+
* and after a save, cancel, edit or delete moves to the matching page.
|
|
145
|
+
* Callbacks given alongside it replace those moves.
|
|
146
|
+
*/
|
|
147
|
+
route?: string;
|
|
78
148
|
showToolbar?: boolean;
|
|
79
149
|
showSidebar?: boolean;
|
|
80
150
|
showTimeline?: boolean;
|
|
@@ -87,56 +157,76 @@ export interface FrappeFormProps<TDoc extends { name: string; doctype: string }>
|
|
|
87
157
|
}
|
|
88
158
|
|
|
89
159
|
// ---------------------------------------------------------------------------
|
|
90
|
-
//
|
|
160
|
+
// Layout source for the form engine
|
|
91
161
|
// ---------------------------------------------------------------------------
|
|
92
162
|
|
|
93
163
|
/**
|
|
94
|
-
*
|
|
164
|
+
* A `sections` override re-expressed as the flat break-delimited field list
|
|
165
|
+
* the form engine lays out, so an overridden layout resolves every field
|
|
166
|
+
* exactly like the meta one.
|
|
95
167
|
*/
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
168
|
+
function sectionsToLayoutFields(sections: FormSection[]): DocTypeField[] {
|
|
169
|
+
const fields: DocTypeField[] = [];
|
|
170
|
+
sections.forEach((section, sectionIndex) => {
|
|
171
|
+
fields.push({
|
|
172
|
+
fieldname: `__section_${sectionIndex}`,
|
|
173
|
+
fieldtype: section.tab ? "Tab Break" : "Section Break",
|
|
174
|
+
label: section.label,
|
|
175
|
+
collapsible: section.collapsible ? 1 : 0,
|
|
176
|
+
});
|
|
177
|
+
section.columns.forEach((column, columnIndex) => {
|
|
178
|
+
if (columnIndex > 0) {
|
|
179
|
+
fields.push({
|
|
180
|
+
fieldname: `__column_${sectionIndex}_${columnIndex}`,
|
|
181
|
+
fieldtype: "Column Break",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
fields.push(...column.fields);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
return fields;
|
|
113
188
|
}
|
|
114
189
|
|
|
190
|
+
// Document bookkeeping, never shown as a field of the record.
|
|
191
|
+
const documentBookkeeping = new Set([
|
|
192
|
+
"name",
|
|
193
|
+
"owner",
|
|
194
|
+
"creation",
|
|
195
|
+
"modified",
|
|
196
|
+
"modified_by",
|
|
197
|
+
"docstatus",
|
|
198
|
+
"idx",
|
|
199
|
+
"doctype",
|
|
200
|
+
"_user_tags",
|
|
201
|
+
"_comments",
|
|
202
|
+
"_assign",
|
|
203
|
+
"_liked_by",
|
|
204
|
+
"_seen",
|
|
205
|
+
]);
|
|
206
|
+
|
|
115
207
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
208
|
+
* A record's own scalar fields as read-only schema fields, for a reader without
|
|
209
|
+
* meta. `$`-keys are the collection's virtual props ($origin, $key,
|
|
210
|
+
* $collectionId), which the row carries but the record does not.
|
|
119
211
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
212
|
+
function fieldsFromDocument(doc: Record<string, unknown>): FieldSchema[] {
|
|
213
|
+
return Object.entries(doc)
|
|
214
|
+
.filter(
|
|
215
|
+
([key, value]) =>
|
|
216
|
+
!key.startsWith("$") &&
|
|
217
|
+
!documentBookkeeping.has(key) &&
|
|
218
|
+
(value === null || typeof value === "string" || typeof value === "number"),
|
|
219
|
+
)
|
|
220
|
+
.map(([key, value]) => ({
|
|
221
|
+
name: key,
|
|
222
|
+
label: key.replace(/_/g, " ").replace(/(^|\s)\S/g, (c) => c.toUpperCase()),
|
|
223
|
+
type: typeof value === "number" ? (Number.isInteger(value) ? "Int" : "Float") : "Data",
|
|
224
|
+
readOnly: true,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
135
227
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
fields: allFields,
|
|
139
|
-
};
|
|
228
|
+
function hasRestrictedPermLevel(meta: DocTypeMeta | undefined): boolean {
|
|
229
|
+
return !!meta?.fields.some((field) => Number(field.permlevel ?? 0) > 0);
|
|
140
230
|
}
|
|
141
231
|
|
|
142
232
|
// ---------------------------------------------------------------------------
|
|
@@ -268,13 +358,75 @@ export function mapDocToSidebarMetadata(
|
|
|
268
358
|
// ---------------------------------------------------------------------------
|
|
269
359
|
|
|
270
360
|
/**
|
|
271
|
-
*
|
|
361
|
+
* The form for a Frappe doctype, resolved from meta by the desk form engine.
|
|
272
362
|
*
|
|
273
|
-
* Fetches
|
|
274
|
-
*
|
|
275
|
-
*
|
|
363
|
+
* Fetches doctype metadata and the document, lays the fields out through
|
|
364
|
+
* `buildFormTabs` (tabs, sections, `depends_on`, `mandatory_depends_on`,
|
|
365
|
+
* `read_only_depends_on`, `collapsible_depends_on`, permlevel,
|
|
366
|
+
* `set_only_once`, `fetch_from` and docstatus), and renders each tab with
|
|
367
|
+
* `SchemaForm`. With a `route` it is the doctype's record page.
|
|
276
368
|
*/
|
|
277
|
-
export function FrappeForm<TDoc extends { name: string; doctype: string }>(
|
|
369
|
+
export function FrappeForm<TDoc extends { name: string; doctype: string }>(
|
|
370
|
+
props: FrappeFormProps<TDoc>,
|
|
371
|
+
) {
|
|
372
|
+
const { route, ...formProps } = props;
|
|
373
|
+
if (route === undefined) return <FrappeFormBody<TDoc> {...formProps} />;
|
|
374
|
+
return <FrappeFormRoute<TDoc> {...formProps} route={route} />;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
type FrappeFormBodyProps<TDoc extends { name: string; doctype: string }> = Omit<
|
|
378
|
+
FrappeFormProps<TDoc>,
|
|
379
|
+
"route"
|
|
380
|
+
> & {
|
|
381
|
+
/** Routed page: the detail template, with a back action to the list. */
|
|
382
|
+
routePage?: { onBack: () => void };
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
function FrappeFormRoute<TDoc extends { name: string; doctype: string }>({
|
|
386
|
+
route,
|
|
387
|
+
name: nameProp,
|
|
388
|
+
onSave,
|
|
389
|
+
onCancel,
|
|
390
|
+
onDelete,
|
|
391
|
+
onEdit,
|
|
392
|
+
...props
|
|
393
|
+
}: Omit<FrappeFormProps<TDoc>, "route"> & { route: string }) {
|
|
394
|
+
const router = useFrappeRouter();
|
|
395
|
+
const fromPath = router ? frappeRouteTarget(route, router.pathname) : undefined;
|
|
396
|
+
const target: FrappeRouteTarget = nameProp
|
|
397
|
+
? { mode: fromPath?.mode === "view" ? "view" : "edit", name: nameProp }
|
|
398
|
+
: fromPath?.mode === "view" || fromPath?.mode === "edit"
|
|
399
|
+
? fromPath
|
|
400
|
+
: { mode: "create" };
|
|
401
|
+
const go = (next: FrappeRouteTarget) => router?.navigate(frappeRouteHref(route, next));
|
|
402
|
+
const name = target.mode === "view" || target.mode === "edit" ? target.name : undefined;
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<FrappeFormBody<TDoc>
|
|
406
|
+
{...props}
|
|
407
|
+
// A new target is a new page: no state carries over from the last one.
|
|
408
|
+
key={`${target.mode}:${name ?? ""}`}
|
|
409
|
+
name={name}
|
|
410
|
+
routePage={{ onBack: () => go({ mode: "list" }) }}
|
|
411
|
+
onEdit={
|
|
412
|
+
target.mode === "view"
|
|
413
|
+
? (onEdit ?? (() => go({ mode: "edit", name: target.name })))
|
|
414
|
+
: undefined
|
|
415
|
+
}
|
|
416
|
+
onSave={
|
|
417
|
+
onSave ??
|
|
418
|
+
((doc) => {
|
|
419
|
+
const saved = doc.name ?? name;
|
|
420
|
+
go(saved ? { mode: "view", name: saved } : { mode: "list" });
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
onCancel={onCancel ?? (() => go(name ? { mode: "view", name } : { mode: "list" }))}
|
|
424
|
+
onDelete={onDelete ?? (() => go({ mode: "list" }))}
|
|
425
|
+
/>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
278
430
|
baseURL,
|
|
279
431
|
auth,
|
|
280
432
|
syncOptions,
|
|
@@ -287,27 +439,42 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
287
439
|
readOnlyFields,
|
|
288
440
|
sections,
|
|
289
441
|
order,
|
|
442
|
+
roles,
|
|
290
443
|
onSave,
|
|
291
444
|
onCancel,
|
|
292
445
|
onDelete,
|
|
446
|
+
onEdit,
|
|
293
447
|
showToolbar = true,
|
|
294
448
|
showSidebar = true,
|
|
295
449
|
showTimeline = true,
|
|
296
450
|
disabled = false,
|
|
297
451
|
sectionHeadingLevel,
|
|
298
|
-
|
|
452
|
+
connections,
|
|
453
|
+
showConnections = true,
|
|
454
|
+
initialDoc,
|
|
455
|
+
loadingFallback,
|
|
456
|
+
notFoundFallback,
|
|
457
|
+
errorFallback,
|
|
458
|
+
routePage,
|
|
459
|
+
}: FrappeFormBodyProps<TDoc>) {
|
|
299
460
|
const config = useFrappeConfig();
|
|
300
461
|
const { knobProps } = useResolvedKnobs();
|
|
301
462
|
const { t } = useTranslation();
|
|
302
|
-
const
|
|
463
|
+
const seedDoc =
|
|
464
|
+
name && initialDoc && (initialDoc as { name?: string }).name === name ? initialDoc : undefined;
|
|
465
|
+
const seedRef = useRef(seedDoc);
|
|
466
|
+
seedRef.current = seedDoc;
|
|
467
|
+
const [localDoc, setLocalDoc] = useState<Partial<TDoc>>(() => seedDoc ?? {});
|
|
303
468
|
/** Last authoritative server snapshot applied to the form (dirty = localDoc ≠ baseline). */
|
|
304
|
-
const [baselineDoc, setBaselineDoc] = useState<Partial<TDoc> | undefined>(
|
|
469
|
+
const [baselineDoc, setBaselineDoc] = useState<Partial<TDoc> | undefined>(() => seedDoc);
|
|
470
|
+
const [docError, setDocError] = useState<Error | null>(null);
|
|
305
471
|
/** Fields the user edited that ALSO changed on the server since baseline. */
|
|
306
472
|
const [conflictFields, setConflictFields] = useState<string[]>([]);
|
|
307
473
|
/** The open document was deleted on the server (doc_deleted / tombstone / eviction). */
|
|
308
474
|
const [wasDeleted, setWasDeleted] = useState(false);
|
|
309
475
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
310
476
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
477
|
+
const [activeTab, setActiveTab] = useState<string | undefined>(undefined);
|
|
311
478
|
const everLoadedRef = useRef(false);
|
|
312
479
|
const localDocRef = useRef(localDoc);
|
|
313
480
|
localDocRef.current = localDoc;
|
|
@@ -352,17 +519,58 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
352
519
|
error: metaError,
|
|
353
520
|
} = useDocTypeMeta(frappeConfig, doctype);
|
|
354
521
|
|
|
355
|
-
|
|
356
|
-
|
|
522
|
+
const sessionRoles = useSessionRoles(
|
|
523
|
+
!roles && hasRestrictedPermLevel(meta) ? frappeConfig : undefined,
|
|
524
|
+
);
|
|
525
|
+
const effectiveRoles = roles ?? sessionRoles;
|
|
526
|
+
const permLevels = useMemo(
|
|
527
|
+
() => buildPermLevelMatrix(meta?.permissions, effectiveRoles),
|
|
528
|
+
[meta?.permissions, effectiveRoles],
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const isCreate = !name;
|
|
532
|
+
const docstatus = Number((baselineDoc as Record<string, unknown> | undefined)?.docstatus ?? 0);
|
|
533
|
+
const layoutFields = useMemo(() => {
|
|
357
534
|
if (!meta) return undefined;
|
|
358
|
-
return
|
|
535
|
+
return sections ? sectionsToLayoutFields(sections) : meta.fields;
|
|
359
536
|
}, [meta, sections]);
|
|
537
|
+
const formTabs = useMemo(() => {
|
|
538
|
+
if (!layoutFields) return undefined;
|
|
539
|
+
return buildFormTabs({
|
|
540
|
+
fields: layoutFields,
|
|
541
|
+
doc: { ...(localDoc as Record<string, unknown>), __islocal: isCreate ? 1 : 0 },
|
|
542
|
+
docstatus,
|
|
543
|
+
formReadOnly: !!meta?.read_only,
|
|
544
|
+
isCreate,
|
|
545
|
+
detailsLabel: t("Details"),
|
|
546
|
+
permLevels,
|
|
547
|
+
hiddenFields: hideFields,
|
|
548
|
+
});
|
|
549
|
+
}, [layoutFields, localDoc, docstatus, meta?.read_only, isCreate, permLevels, hideFields, t]);
|
|
550
|
+
// Meta is permission-gated (Guest gets 403) while the record may still be
|
|
551
|
+
// readable: the form then lays the record's own fields out, read-only.
|
|
552
|
+
const readsWithoutMeta = !children && !meta && Boolean(metaError) && Boolean(name);
|
|
553
|
+
const recordTabs = useMemo((): FormTab[] | undefined => {
|
|
554
|
+
if (!readsWithoutMeta || !baselineDoc) return undefined;
|
|
555
|
+
const recordFields = fieldsFromDocument(baselineDoc as Record<string, unknown>);
|
|
556
|
+
return [
|
|
557
|
+
{
|
|
558
|
+
key: "__details",
|
|
559
|
+
label: t("Details"),
|
|
560
|
+
schema: { sections: [{ columns: [{ fields: recordFields }] }], fields: recordFields },
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
}, [readsWithoutMeta, baselineDoc, t]);
|
|
564
|
+
const promptNamed = isCreate && !!meta && isPromptNamed(meta.autoname, meta.naming_rule);
|
|
565
|
+
const fetchMap = useMemo(() => (meta ? buildFetchMap(meta.fields) : undefined), [meta]);
|
|
360
566
|
|
|
361
|
-
const
|
|
362
|
-
|
|
567
|
+
const handleDocSyncError = useCallback((err: unknown) => {
|
|
568
|
+
setDocError(err instanceof Error ? err : new Error(String(err)));
|
|
569
|
+
}, []);
|
|
363
570
|
const docCollection = useFrappeCollection<TDoc>(name ? frappeConfig : undefined, doctype, {
|
|
364
571
|
fields,
|
|
365
572
|
filters: name ? { name } : undefined,
|
|
573
|
+
onSyncError: handleDocSyncError,
|
|
366
574
|
});
|
|
367
575
|
const { data: rawDocData, isLoading: isDocLoading } = useLiveQuery(
|
|
368
576
|
(q) => {
|
|
@@ -440,9 +648,53 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
440
648
|
everLoadedRef.current = false;
|
|
441
649
|
setWasDeleted(false);
|
|
442
650
|
setConflictFields([]);
|
|
443
|
-
|
|
651
|
+
setDocError(null);
|
|
652
|
+
const seed = seedRef.current;
|
|
653
|
+
setBaselineDoc(seed);
|
|
654
|
+
if (seed) setLocalDoc(seed);
|
|
444
655
|
}, [doctype, name]);
|
|
445
656
|
|
|
657
|
+
const effectiveConnections = useMemo(
|
|
658
|
+
() => connections ?? connectionsFromMeta(meta),
|
|
659
|
+
[connections, meta],
|
|
660
|
+
);
|
|
661
|
+
const readsConnections = Boolean(children) && Boolean(name) && effectiveConnections.length > 0;
|
|
662
|
+
const [connectionSnapshots, setConnectionSnapshots] = useState<
|
|
663
|
+
Record<string, FrappeRowsSnapshot<Record<string, unknown> & { name: string; doctype: string }>>
|
|
664
|
+
>({});
|
|
665
|
+
useEffect(() => {
|
|
666
|
+
setConnectionSnapshots({});
|
|
667
|
+
}, [doctype, name]);
|
|
668
|
+
const connectionReads = useMemo(
|
|
669
|
+
() =>
|
|
670
|
+
readsConnections && name
|
|
671
|
+
? effectiveConnections.map((link) => ({
|
|
672
|
+
link,
|
|
673
|
+
options: {
|
|
674
|
+
fields: connectionFields(link),
|
|
675
|
+
filters: [[link.fieldname, "=", name]] as Array<[string, string, unknown]>,
|
|
676
|
+
limit: link.limit ?? 0,
|
|
677
|
+
},
|
|
678
|
+
onChange: (
|
|
679
|
+
snapshot: FrappeRowsSnapshot<
|
|
680
|
+
Record<string, unknown> & { name: string; doctype: string }
|
|
681
|
+
>,
|
|
682
|
+
) => setConnectionSnapshots((prev) => ({ ...prev, [link.doctype]: snapshot })),
|
|
683
|
+
}))
|
|
684
|
+
: [],
|
|
685
|
+
[readsConnections, effectiveConnections, name],
|
|
686
|
+
);
|
|
687
|
+
const connectionRows = useMemo(() => {
|
|
688
|
+
const rows: FrappeConnectionRows = {};
|
|
689
|
+
for (const { link } of connectionReads) {
|
|
690
|
+
rows[link.doctype] = connectionSnapshots[link.doctype]?.rows ?? [];
|
|
691
|
+
}
|
|
692
|
+
return rows;
|
|
693
|
+
}, [connectionReads, connectionSnapshots]);
|
|
694
|
+
const connectionsLoading = connectionReads.some(
|
|
695
|
+
({ link }) => connectionSnapshots[link.doctype]?.isLoading ?? true,
|
|
696
|
+
);
|
|
697
|
+
|
|
446
698
|
// Immediate honest deleted state: the live app emits doc_deleted for the
|
|
447
699
|
// open doc's doctype room; tombstone backfill / cache eviction below is the
|
|
448
700
|
// durable fallback for deletions that happen while disconnected.
|
|
@@ -508,6 +760,38 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
508
760
|
}, [localDoc, baselineDoc]);
|
|
509
761
|
const hasUnsavedChanges = dirtyFieldNames.length > 0;
|
|
510
762
|
|
|
763
|
+
// fetch_from preview: a changed Link pulls its dependent columns off the
|
|
764
|
+
// linked doc (validate_link on a bench, the fixture corpus otherwise) and
|
|
765
|
+
// patches the targets; clearing the link blanks them.
|
|
766
|
+
const runFetchFrom = (field: string, value: unknown) => {
|
|
767
|
+
const rules = fetchMap?.get(field);
|
|
768
|
+
if (!rules || rules.length === 0) return;
|
|
769
|
+
const applyPatch = (fetched: Record<string, unknown>) => {
|
|
770
|
+
setLocalDoc((prev) => {
|
|
771
|
+
const current = prev as Record<string, unknown>;
|
|
772
|
+
if (current[field] !== value) return prev;
|
|
773
|
+
const patch = applyFetchedValues(rules, fetched, current);
|
|
774
|
+
return Object.keys(patch).length > 0 ? ({ ...current, ...patch } as Partial<TDoc>) : prev;
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
|
+
if (isEmptyFieldValue(value)) {
|
|
778
|
+
applyPatch({});
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const linkField = meta?.fields.find((metaField) => metaField.fieldname === field);
|
|
782
|
+
const targetDoctype = typeof linkField?.options === "string" ? linkField.options : "";
|
|
783
|
+
if (!targetDoctype) return;
|
|
784
|
+
const sources = Array.from(new Set(rules.map((rule) => rule.source)));
|
|
785
|
+
const fixtures = frappeConfig.fixtures;
|
|
786
|
+
const fetched = fixtures
|
|
787
|
+
? fixtures
|
|
788
|
+
.getDoc(targetDoctype, String(value), sources)
|
|
789
|
+
.then((doc) => (doc ?? {}) as Record<string, unknown>)
|
|
790
|
+
: validateLink(createHttpClient(frappeConfig), targetDoctype, String(value), sources);
|
|
791
|
+
// An unknown link leaves the targets as they are; the server re-validates on save.
|
|
792
|
+
fetched.then(applyPatch).catch(() => undefined);
|
|
793
|
+
};
|
|
794
|
+
|
|
511
795
|
const setValue = (field: keyof TDoc, value: unknown) => {
|
|
512
796
|
setLocalDoc((prev) => ({
|
|
513
797
|
...prev,
|
|
@@ -518,20 +802,27 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
518
802
|
delete newErrors[String(field)];
|
|
519
803
|
return newErrors;
|
|
520
804
|
});
|
|
805
|
+
runFetchFrom(String(field), value);
|
|
521
806
|
};
|
|
522
807
|
|
|
808
|
+
// Required = what the engine resolved: reqd or a truthy
|
|
809
|
+
// mandatory_depends_on, on fields that are actually laid out (depends_on,
|
|
810
|
+
// permlevel and host-hidden fields never reach the tabs).
|
|
523
811
|
const validateRequiredFields = (): Record<string, string> => {
|
|
524
|
-
if (!
|
|
812
|
+
if (!formTabs) return {};
|
|
525
813
|
const validationErrors: Record<string, string> = {};
|
|
526
814
|
const doc = localDoc as Record<string, unknown>;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
815
|
+
if (promptNamed && isEmptyFieldValue(doc.name)) {
|
|
816
|
+
validationErrors.name = t("Name is required");
|
|
817
|
+
}
|
|
818
|
+
for (const tab of formTabs) {
|
|
819
|
+
for (const field of tab.schema.fields) {
|
|
820
|
+
if (!field.required) continue;
|
|
821
|
+
if (isEmptyFieldValue(doc[field.name])) {
|
|
822
|
+
validationErrors[field.name] = t("{{label}} is required", {
|
|
823
|
+
label: field.label || field.name,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
535
826
|
}
|
|
536
827
|
}
|
|
537
828
|
return validationErrors;
|
|
@@ -567,15 +858,15 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
567
858
|
setConflictFields([]);
|
|
568
859
|
} else {
|
|
569
860
|
if (!collection) return;
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
name:
|
|
574
|
-
((localDoc as Record<string, unknown>).name as string | undefined) ??
|
|
575
|
-
`new-${Date.now()}`,
|
|
576
|
-
} as TDoc;
|
|
861
|
+
const temporaryName =
|
|
862
|
+
((localDoc as Record<string, unknown>).name as string | undefined) ?? `new-${Date.now()}`;
|
|
863
|
+
const docToInsert = { ...localDoc, doctype, name: temporaryName } as TDoc;
|
|
577
864
|
const tx = collection.insert(docToInsert);
|
|
578
865
|
await tx.isPersisted.promise;
|
|
866
|
+
const createdName =
|
|
867
|
+
getFrappeSyncModule(frappeConfig)?.createdName(temporaryName) ?? temporaryName;
|
|
868
|
+
onSave?.({ ...localDoc, name: createdName } as Partial<TDoc>);
|
|
869
|
+
return;
|
|
579
870
|
}
|
|
580
871
|
onSave?.(localDoc as Partial<TDoc>);
|
|
581
872
|
} catch (err: unknown) {
|
|
@@ -634,17 +925,46 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
634
925
|
}
|
|
635
926
|
};
|
|
636
927
|
|
|
928
|
+
const titleField = meta?.title_field;
|
|
929
|
+
const titleValue = titleField ? (localDoc as Record<string, unknown>)[titleField] : undefined;
|
|
930
|
+
const pageTitle = isCreate ? t("New {{doctype}}", { doctype }) : String(titleValue || name);
|
|
931
|
+
const frame = (content: React.ReactNode, aside?: React.ReactNode) =>
|
|
932
|
+
routePage ? (
|
|
933
|
+
// flexBasis auto: see DeskFormPage (react-native-web's min-height:0
|
|
934
|
+
// reset would cap the scroll page at viewport height).
|
|
935
|
+
<DetailPageLayout
|
|
936
|
+
flexBasis="auto"
|
|
937
|
+
toolbarLeading={
|
|
938
|
+
<Button
|
|
939
|
+
size="$3"
|
|
940
|
+
chromeless
|
|
941
|
+
icon={ArrowLeftIcon}
|
|
942
|
+
onPress={routePage.onBack}
|
|
943
|
+
aria-label={t("Back to {{doctype}} list", { doctype })}
|
|
944
|
+
>
|
|
945
|
+
{doctype}
|
|
946
|
+
</Button>
|
|
947
|
+
}
|
|
948
|
+
header={<PageHeader title={pageTitle} divider={false} />}
|
|
949
|
+
aside={aside}
|
|
950
|
+
>
|
|
951
|
+
{content}
|
|
952
|
+
</DetailPageLayout>
|
|
953
|
+
) : (
|
|
954
|
+
content
|
|
955
|
+
);
|
|
956
|
+
|
|
637
957
|
// Schema-driven mode needs meta; render-prop mode can proceed without it.
|
|
638
958
|
if (!children && isMetaLoading && !meta) {
|
|
639
|
-
return (
|
|
959
|
+
return frame(
|
|
640
960
|
<YStack {...knobProps.panelPadding} testID="frappe-form-skeleton">
|
|
641
961
|
<AsyncSkeleton layout="generic" />
|
|
642
|
-
</YStack
|
|
962
|
+
</YStack>,
|
|
643
963
|
);
|
|
644
964
|
}
|
|
645
965
|
|
|
646
|
-
if (!children && metaError && !meta) {
|
|
647
|
-
return (
|
|
966
|
+
if (!children && metaError && !meta && !name) {
|
|
967
|
+
return frame(
|
|
648
968
|
<YStack {...knobProps.panelPadding} alignItems="center" {...knobProps.gap}>
|
|
649
969
|
<Text color={formCommonColors.error} {...knobProps.body}>
|
|
650
970
|
{t("Failed to load {{doctype}} form", { doctype })}
|
|
@@ -652,22 +972,40 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
652
972
|
<Text color={formCommonColors.muted} {...knobProps.body}>
|
|
653
973
|
{metaError.message}
|
|
654
974
|
</Text>
|
|
655
|
-
</YStack
|
|
975
|
+
</YStack>,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// No collection yet (server render, hydration pass) reads as loading: the
|
|
980
|
+
// document is not known to be missing until the live read answers.
|
|
981
|
+
const isDocReading = Boolean(name) && (!docCollection || Boolean(liveDoc?.isLoading));
|
|
982
|
+
const showsSeed = Boolean(seedDoc) && isDocReading;
|
|
983
|
+
|
|
984
|
+
if (name && docError && !liveDoc?.data && !wasDeleted) {
|
|
985
|
+
if (errorFallback !== undefined) return frame(errorFallback);
|
|
986
|
+
return frame(
|
|
987
|
+
<AsyncBoundary
|
|
988
|
+
{...knobProps.panelPadding}
|
|
989
|
+
error={docError}
|
|
990
|
+
errorTitle={t("Failed to load {{doctype}}", { doctype })}
|
|
991
|
+
/>,
|
|
656
992
|
);
|
|
657
993
|
}
|
|
658
994
|
|
|
659
|
-
if (name &&
|
|
660
|
-
return (
|
|
995
|
+
if (name && isDocReading && !showsSeed && !wasDeleted) {
|
|
996
|
+
if (loadingFallback !== undefined) return frame(loadingFallback);
|
|
997
|
+
return frame(
|
|
661
998
|
<YStack {...knobProps.panelPadding} testID="frappe-form-skeleton">
|
|
662
999
|
<AsyncSkeleton layout="generic" />
|
|
663
|
-
</YStack
|
|
1000
|
+
</YStack>,
|
|
664
1001
|
);
|
|
665
1002
|
}
|
|
666
1003
|
|
|
667
1004
|
// Never-loaded miss (bad name, or read denied) — distinct from the
|
|
668
1005
|
// deleted-while-open state below, which keeps the form mounted.
|
|
669
|
-
if (name && !
|
|
670
|
-
return (
|
|
1006
|
+
if (name && !isDocReading && !liveDoc?.data && !wasDeleted) {
|
|
1007
|
+
if (notFoundFallback !== undefined) return frame(notFoundFallback);
|
|
1008
|
+
return frame(
|
|
671
1009
|
<YStack {...knobProps.panelPadding} alignItems="center" {...knobProps.gap}>
|
|
672
1010
|
<Text color={formCommonColors.error} {...knobProps.body}>
|
|
673
1011
|
{t("{{doctype}} not found", { doctype })}
|
|
@@ -675,11 +1013,14 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
675
1013
|
<Text color={formCommonColors.muted} {...knobProps.body}>
|
|
676
1014
|
{t("Document {{name}} does not exist or could not be loaded.", { name })}
|
|
677
1015
|
</Text>
|
|
678
|
-
</YStack
|
|
1016
|
+
</YStack>,
|
|
679
1017
|
);
|
|
680
1018
|
}
|
|
681
1019
|
|
|
682
1020
|
const effectiveDisabled = disabled || wasDeleted;
|
|
1021
|
+
const readOnlyView = Boolean(onEdit) || readsWithoutMeta;
|
|
1022
|
+
const fieldsDisabled = effectiveDisabled || readOnlyView;
|
|
1023
|
+
const shownTabs = formTabs ?? recordTabs;
|
|
683
1024
|
const imageFieldName = meta?.image_field;
|
|
684
1025
|
const sidebarImageUrl = imageFieldName
|
|
685
1026
|
? resolveFileUrl((localDoc as Record<string, unknown>)[imageFieldName], finalBaseURL)
|
|
@@ -691,13 +1032,73 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
691
1032
|
})
|
|
692
1033
|
.join(", ");
|
|
693
1034
|
|
|
694
|
-
|
|
1035
|
+
const formTabItems: TabsItem[] = (shownTabs ?? []).map((tab) => ({
|
|
1036
|
+
value: tab.key,
|
|
1037
|
+
label: tab.label,
|
|
1038
|
+
content: (
|
|
1039
|
+
<SchemaForm
|
|
1040
|
+
schema={tab.schema}
|
|
1041
|
+
doc={localDoc as Record<string, any>}
|
|
1042
|
+
setValue={(field, value) => setValue(field as keyof TDoc, value)}
|
|
1043
|
+
errors={errors}
|
|
1044
|
+
disabled={fieldsDisabled}
|
|
1045
|
+
overrides={overrides}
|
|
1046
|
+
readOnlyFields={readOnlyFields}
|
|
1047
|
+
sectionHeadingLevel={sectionHeadingLevel}
|
|
1048
|
+
order={order}
|
|
1049
|
+
/>
|
|
1050
|
+
),
|
|
1051
|
+
}));
|
|
1052
|
+
const tabItems: TabsItem[] =
|
|
1053
|
+
showConnections && name && effectiveConnections.length > 0
|
|
1054
|
+
? [
|
|
1055
|
+
...formTabItems,
|
|
1056
|
+
{
|
|
1057
|
+
value: "__connections",
|
|
1058
|
+
label: t("Connections"),
|
|
1059
|
+
content: (
|
|
1060
|
+
<FrappeFormConnections
|
|
1061
|
+
connection={frappeConfig}
|
|
1062
|
+
connections={effectiveConnections}
|
|
1063
|
+
name={name}
|
|
1064
|
+
/>
|
|
1065
|
+
),
|
|
1066
|
+
},
|
|
1067
|
+
]
|
|
1068
|
+
: formTabItems;
|
|
1069
|
+
const effectiveTab =
|
|
1070
|
+
activeTab && tabItems.some((tab) => tab.value === activeTab) ? activeTab : tabItems[0]?.value;
|
|
1071
|
+
const localName = (localDoc as Record<string, unknown>).name;
|
|
1072
|
+
const sidebar =
|
|
1073
|
+
showSidebar && name && meta ? (
|
|
1074
|
+
<FormSidebar
|
|
1075
|
+
imageUrl={sidebarImageUrl}
|
|
1076
|
+
sections={buildSidebarSections(
|
|
1077
|
+
localDoc as Record<string, any>,
|
|
1078
|
+
shareRows,
|
|
1079
|
+
t,
|
|
1080
|
+
assignmentRows,
|
|
1081
|
+
)}
|
|
1082
|
+
metadata={mapDocToSidebarMetadata(localDoc as Record<string, any>, t)}
|
|
1083
|
+
/>
|
|
1084
|
+
) : undefined;
|
|
1085
|
+
|
|
1086
|
+
return frame(
|
|
695
1087
|
// flexBasis auto (not the flex:1 shorthand's 0%): inside scroll-flow page
|
|
696
1088
|
// scaffolds (LC-29 templates) the parent is auto-height, and a 0% basis
|
|
697
1089
|
// collapses this stack to zero height on web — content then paints over
|
|
698
1090
|
// whatever follows. Bounded parents still fill (grow 1) exactly as before.
|
|
699
1091
|
<YStack {...knobProps.gapLg} flexGrow={1} flexShrink={1} flexBasis="auto">
|
|
700
1092
|
<FrappeReconnectIndicator config={frappeConfig} />
|
|
1093
|
+
{connectionReads.map((read) => (
|
|
1094
|
+
<FrappeRowsReader
|
|
1095
|
+
key={read.link.doctype}
|
|
1096
|
+
connection={frappeConfig}
|
|
1097
|
+
doctype={read.link.doctype}
|
|
1098
|
+
options={read.options as never}
|
|
1099
|
+
onChange={read.onChange}
|
|
1100
|
+
/>
|
|
1101
|
+
))}
|
|
701
1102
|
|
|
702
1103
|
{wasDeleted && (
|
|
703
1104
|
<XStack
|
|
@@ -742,19 +1143,9 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
742
1143
|
{/* flexBasis auto — see the root stack note (zero-height collapse in
|
|
743
1144
|
auto-height scroll flows would paint the Timeline over the form). */}
|
|
744
1145
|
<XStack flexGrow={1} flexShrink={1} flexBasis="auto" {...knobProps.gapLg}>
|
|
745
|
-
{/* Sidebar (edit mode only) — assignments/shares/image are live
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
imageUrl={sidebarImageUrl}
|
|
749
|
-
sections={buildSidebarSections(
|
|
750
|
-
localDoc as Record<string, any>,
|
|
751
|
-
shareRows,
|
|
752
|
-
t,
|
|
753
|
-
assignmentRows,
|
|
754
|
-
)}
|
|
755
|
-
metadata={mapDocToSidebarMetadata(localDoc as Record<string, any>, t)}
|
|
756
|
-
/>
|
|
757
|
-
)}
|
|
1146
|
+
{/* Sidebar (edit mode only) — assignments/shares/image are live.
|
|
1147
|
+
A routed page carries it in the template's aside instead. */}
|
|
1148
|
+
{routePage ? null : sidebar}
|
|
758
1149
|
|
|
759
1150
|
{/* Form body */}
|
|
760
1151
|
<YStack flex={1} {...knobProps.gapLg}>
|
|
@@ -766,22 +1157,28 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
766
1157
|
isSubmitting,
|
|
767
1158
|
errors,
|
|
768
1159
|
meta,
|
|
769
|
-
disabled:
|
|
1160
|
+
disabled: fieldsDisabled,
|
|
1161
|
+
connections: connectionRows,
|
|
1162
|
+
connectionsLoading,
|
|
770
1163
|
})
|
|
771
|
-
) :
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1164
|
+
) : shownTabs ? (
|
|
1165
|
+
<YStack {...knobProps.gapLg}>
|
|
1166
|
+
{promptNamed && (
|
|
1167
|
+
<Input
|
|
1168
|
+
label={t("Name")}
|
|
1169
|
+
value={typeof localName === "string" ? localName : ""}
|
|
1170
|
+
onChangeText={(value) => setValue("name" as keyof TDoc, value)}
|
|
1171
|
+
placeholder={t("New document name")}
|
|
1172
|
+
error={errors.name}
|
|
1173
|
+
disabled={effectiveDisabled}
|
|
1174
|
+
/>
|
|
1175
|
+
)}
|
|
1176
|
+
{tabItems.length > 1 ? (
|
|
1177
|
+
<Tabs items={tabItems} value={effectiveTab} onChange={setActiveTab} lazyMount />
|
|
1178
|
+
) : (
|
|
1179
|
+
(tabItems[0]?.content ?? null)
|
|
1180
|
+
)}
|
|
1181
|
+
</YStack>
|
|
785
1182
|
) : (
|
|
786
1183
|
// Metadata unavailable (should be covered by metaError branch above)
|
|
787
1184
|
<YStack {...knobProps.panelPadding} alignItems="center">
|
|
@@ -797,14 +1194,14 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
797
1194
|
</Text>
|
|
798
1195
|
)}
|
|
799
1196
|
|
|
800
|
-
{showToolbar && (
|
|
1197
|
+
{showToolbar && !readsWithoutMeta && (
|
|
801
1198
|
<XStack
|
|
802
1199
|
{...knobProps.gap}
|
|
803
1200
|
justifyContent="flex-end"
|
|
804
1201
|
alignItems="center"
|
|
805
1202
|
paddingTop="$3"
|
|
806
1203
|
>
|
|
807
|
-
{hasUnsavedChanges && (
|
|
1204
|
+
{hasUnsavedChanges && !onEdit && (
|
|
808
1205
|
<Text color="$orange11" marginInlineEnd="auto" {...knobProps.body}>
|
|
809
1206
|
{t("Not Saved")}
|
|
810
1207
|
</Text>
|
|
@@ -825,37 +1222,55 @@ export function FrappeForm<TDoc extends { name: string; doctype: string }>({
|
|
|
825
1222
|
</Button>
|
|
826
1223
|
)}
|
|
827
1224
|
|
|
828
|
-
{
|
|
1225
|
+
{onEdit ? (
|
|
829
1226
|
<Button
|
|
830
|
-
|
|
831
|
-
icon={
|
|
832
|
-
disabled={
|
|
1227
|
+
accent
|
|
1228
|
+
icon={PencilSimpleIcon}
|
|
1229
|
+
disabled={effectiveDisabled}
|
|
833
1230
|
disabledReason={t("Form is read-only")}
|
|
834
1231
|
disabledReasonPlacement="inline"
|
|
835
|
-
onPress={
|
|
1232
|
+
onPress={onEdit}
|
|
836
1233
|
>
|
|
837
|
-
{t("
|
|
1234
|
+
{t("Edit")}
|
|
838
1235
|
</Button>
|
|
839
|
-
)
|
|
1236
|
+
) : (
|
|
1237
|
+
<>
|
|
1238
|
+
{onCancel && (
|
|
1239
|
+
<Button
|
|
1240
|
+
outlined
|
|
1241
|
+
icon={XIcon}
|
|
1242
|
+
disabled={disabled}
|
|
1243
|
+
disabledReason={t("Form is read-only")}
|
|
1244
|
+
disabledReasonPlacement="inline"
|
|
1245
|
+
onPress={handleCancel}
|
|
1246
|
+
>
|
|
1247
|
+
{t("Cancel")}
|
|
1248
|
+
</Button>
|
|
1249
|
+
)}
|
|
840
1250
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1251
|
+
<Button
|
|
1252
|
+
accent
|
|
1253
|
+
icon={FloppyDiskIcon}
|
|
1254
|
+
loading={isSubmitting}
|
|
1255
|
+
disabled={effectiveDisabled}
|
|
1256
|
+
disabledReason={t("Form is read-only")}
|
|
1257
|
+
disabledReasonPlacement="inline"
|
|
1258
|
+
onPress={handleSave}
|
|
1259
|
+
>
|
|
1260
|
+
{isSubmitting ? t("Saving...") : t("Save")}
|
|
1261
|
+
</Button>
|
|
1262
|
+
</>
|
|
1263
|
+
)}
|
|
852
1264
|
</XStack>
|
|
853
1265
|
)}
|
|
854
1266
|
</YStack>
|
|
855
1267
|
</XStack>
|
|
856
1268
|
|
|
857
1269
|
{/* Timeline (edit mode only) — live Comment/Version via useFrappeCollection */}
|
|
858
|
-
{showTimeline && name &&
|
|
859
|
-
|
|
1270
|
+
{showTimeline && name && !readsWithoutMeta && (
|
|
1271
|
+
<Timeline doctype={doctype} docname={name} label={t("Activity")} />
|
|
1272
|
+
)}
|
|
1273
|
+
</YStack>,
|
|
1274
|
+
routePage ? sidebar : undefined,
|
|
860
1275
|
);
|
|
861
1276
|
}
|