@multiplatform.one/backoffice 7.28.1 → 7.29.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 +1 -1
- package/dist/cjs/desk/DeskSurface.native.js.map +1 -1
- package/dist/cjs/form/DocFormScreen.cjs +167 -224
- package/dist/cjs/form/DocFormScreen.native.js +182 -259
- package/dist/cjs/form/DocFormScreen.native.js.map +1 -1
- package/dist/cjs/i18n/en.cjs +1 -0
- package/dist/cjs/i18n/en.native.js +1 -0
- package/dist/cjs/i18n/en.native.js.map +1 -1
- package/dist/cjs/list/ListBulkActionsBar.cjs +30 -18
- package/dist/cjs/list/ListBulkActionsBar.native.js +43 -31
- package/dist/cjs/list/ListBulkActionsBar.native.js.map +1 -1
- package/dist/cjs/list/sidebar-section.cjs +15 -7
- package/dist/cjs/list/sidebar-section.native.js +16 -7
- package/dist/cjs/list/sidebar-section.native.js.map +1 -1
- package/dist/cjs/report/DoctypeReportView.cjs +20 -374
- package/dist/cjs/report/DoctypeReportView.native.js +23 -455
- package/dist/cjs/report/DoctypeReportView.native.js.map +1 -1
- package/dist/cjs/report/QueryReportScreen.cjs +33 -23
- package/dist/cjs/report/QueryReportScreen.native.js +33 -23
- package/dist/cjs/report/QueryReportScreen.native.js.map +1 -1
- package/dist/cjs/report/queryReportColumns.cjs +3 -105
- package/dist/cjs/report/queryReportColumns.native.js +2 -129
- package/dist/cjs/report/queryReportColumns.native.js.map +1 -1
- package/dist/cjs/report/queryReportFilterBar.cjs +3 -4
- package/dist/cjs/report/queryReportFilterBar.native.js +3 -4
- package/dist/cjs/report/queryReportFilterBar.native.js.map +1 -1
- package/dist/cjs/report/queryReportUrlState.cjs +2 -2
- package/dist/cjs/report/queryReportUrlState.native.js +2 -2
- package/dist/cjs/report/queryReportUrlState.native.js.map +1 -1
- package/dist/cjs/report/report-grid.cjs +2 -426
- package/dist/cjs/report/report-grid.native.js +1 -479
- package/dist/cjs/report/report-grid.native.js.map +1 -1
- package/dist/cjs/report/report-toolbar.cjs +2 -228
- package/dist/cjs/report/report-toolbar.native.js +1 -274
- package/dist/cjs/report/report-toolbar.native.js.map +1 -1
- package/dist/cjs/report/reportShapes.cjs +12 -383
- package/dist/cjs/report/reportShapes.native.js +12 -501
- package/dist/cjs/report/reportShapes.native.js.map +1 -1
- package/dist/cjs/report/reportViewModel.cjs +20 -360
- package/dist/cjs/report/reportViewModel.native.js +19 -627
- package/dist/cjs/report/reportViewModel.native.js.map +1 -1
- package/dist/cjs/report/savedReports.cjs +2 -106
- package/dist/cjs/report/savedReports.native.js +1 -132
- package/dist/cjs/report/savedReports.native.js.map +1 -1
- package/dist/cjs/report/useQueryReport.cjs +11 -219
- package/dist/cjs/report/useQueryReport.native.js +11 -244
- package/dist/cjs/report/useQueryReport.native.js.map +1 -1
- package/dist/cjs/shared/measures.cjs +3 -5
- package/dist/cjs/shared/measures.native.js +3 -5
- package/dist/cjs/shared/measures.native.js.map +1 -1
- package/dist/cjs/views/ReportView.cjs +3 -256
- package/dist/cjs/views/ReportView.native.js +3 -382
- package/dist/cjs/views/ReportView.native.js.map +1 -1
- package/dist/cjs/views/report-controls.cjs +6 -6
- package/dist/cjs/views/report-controls.native.js +6 -6
- package/dist/cjs/views/report-controls.native.js.map +1 -1
- package/dist/esm/desk/DeskSurface.mjs.map +1 -1
- package/dist/esm/desk/DeskSurface.native.js.map +1 -1
- package/dist/esm/form/DocFormScreen.mjs +169 -226
- package/dist/esm/form/DocFormScreen.mjs.map +1 -1
- package/dist/esm/form/DocFormScreen.native.js +184 -261
- package/dist/esm/form/DocFormScreen.native.js.map +1 -1
- package/dist/esm/i18n/en.mjs +1 -0
- package/dist/esm/i18n/en.mjs.map +1 -1
- package/dist/esm/i18n/en.native.js +1 -0
- package/dist/esm/i18n/en.native.js.map +1 -1
- package/dist/esm/list/ListBulkActionsBar.mjs +31 -19
- package/dist/esm/list/ListBulkActionsBar.mjs.map +1 -1
- package/dist/esm/list/ListBulkActionsBar.native.js +37 -25
- package/dist/esm/list/ListBulkActionsBar.native.js.map +1 -1
- package/dist/esm/list/sidebar-section.mjs +16 -8
- package/dist/esm/list/sidebar-section.mjs.map +1 -1
- package/dist/esm/list/sidebar-section.native.js +17 -8
- package/dist/esm/list/sidebar-section.native.js.map +1 -1
- package/dist/esm/report/DoctypeReportView.mjs +21 -375
- package/dist/esm/report/DoctypeReportView.mjs.map +1 -1
- package/dist/esm/report/DoctypeReportView.native.js +24 -456
- package/dist/esm/report/DoctypeReportView.native.js.map +1 -1
- package/dist/esm/report/QueryReportScreen.mjs +26 -16
- package/dist/esm/report/QueryReportScreen.mjs.map +1 -1
- package/dist/esm/report/QueryReportScreen.native.js +26 -16
- package/dist/esm/report/QueryReportScreen.native.js.map +1 -1
- package/dist/esm/report/queryReportColumns.mjs +1 -103
- package/dist/esm/report/queryReportColumns.mjs.map +1 -1
- package/dist/esm/report/queryReportColumns.native.js +1 -117
- package/dist/esm/report/queryReportColumns.native.js.map +1 -1
- package/dist/esm/report/queryReportFilterBar.mjs +1 -2
- package/dist/esm/report/queryReportFilterBar.mjs.map +1 -1
- package/dist/esm/report/queryReportFilterBar.native.js +1 -2
- package/dist/esm/report/queryReportFilterBar.native.js.map +1 -1
- package/dist/esm/report/queryReportUrlState.mjs +1 -1
- package/dist/esm/report/queryReportUrlState.native.js +1 -1
- package/dist/esm/report/report-grid.mjs +1 -425
- package/dist/esm/report/report-grid.mjs.map +1 -1
- package/dist/esm/report/report-grid.native.js +1 -468
- package/dist/esm/report/report-grid.native.js.map +1 -1
- package/dist/esm/report/report-toolbar.mjs +2 -228
- package/dist/esm/report/report-toolbar.mjs.map +1 -1
- package/dist/esm/report/report-toolbar.native.js +2 -264
- package/dist/esm/report/report-toolbar.native.js.map +1 -1
- package/dist/esm/report/reportShapes.mjs +2 -373
- package/dist/esm/report/reportShapes.mjs.map +1 -1
- package/dist/esm/report/reportShapes.native.js +2 -491
- package/dist/esm/report/reportShapes.native.js.map +1 -1
- package/dist/esm/report/reportViewModel.mjs +2 -342
- package/dist/esm/report/reportViewModel.mjs.map +1 -1
- package/dist/esm/report/reportViewModel.native.js +2 -610
- package/dist/esm/report/reportViewModel.native.js.map +1 -1
- package/dist/esm/report/savedReports.mjs +1 -105
- package/dist/esm/report/savedReports.mjs.map +1 -1
- package/dist/esm/report/savedReports.native.js +1 -132
- package/dist/esm/report/savedReports.native.js.map +1 -1
- package/dist/esm/report/useQueryReport.mjs +2 -210
- package/dist/esm/report/useQueryReport.mjs.map +1 -1
- package/dist/esm/report/useQueryReport.native.js +2 -235
- package/dist/esm/report/useQueryReport.native.js.map +1 -1
- package/dist/esm/shared/measures.mjs +1 -3
- package/dist/esm/shared/measures.mjs.map +1 -1
- package/dist/esm/shared/measures.native.js +1 -3
- package/dist/esm/shared/measures.native.js.map +1 -1
- package/dist/esm/views/ReportView.mjs +3 -256
- package/dist/esm/views/ReportView.mjs.map +1 -1
- package/dist/esm/views/ReportView.native.js +3 -382
- package/dist/esm/views/ReportView.native.js.map +1 -1
- package/dist/esm/views/report-controls.mjs +3 -3
- package/dist/esm/views/report-controls.mjs.map +1 -1
- package/dist/esm/views/report-controls.native.js +3 -3
- package/dist/esm/views/report-controls.native.js.map +1 -1
- package/dist/jsx/desk/DeskSurface.mjs.map +1 -1
- package/dist/jsx/desk/DeskSurface.native.js.map +1 -1
- package/dist/jsx/form/DocFormScreen.mjs +169 -226
- package/dist/jsx/form/DocFormScreen.mjs.map +1 -1
- package/dist/jsx/form/DocFormScreen.native.js +182 -259
- package/dist/jsx/form/DocFormScreen.native.js.map +1 -1
- package/dist/jsx/i18n/en.mjs +1 -0
- package/dist/jsx/i18n/en.mjs.map +1 -1
- package/dist/jsx/i18n/en.native.js +1 -0
- package/dist/jsx/i18n/en.native.js.map +1 -1
- package/dist/jsx/list/ListBulkActionsBar.mjs +31 -19
- package/dist/jsx/list/ListBulkActionsBar.mjs.map +1 -1
- package/dist/jsx/list/ListBulkActionsBar.native.js +43 -31
- package/dist/jsx/list/ListBulkActionsBar.native.js.map +1 -1
- package/dist/jsx/list/sidebar-section.mjs +16 -8
- package/dist/jsx/list/sidebar-section.mjs.map +1 -1
- package/dist/jsx/list/sidebar-section.native.js +16 -7
- package/dist/jsx/list/sidebar-section.native.js.map +1 -1
- package/dist/jsx/report/DoctypeReportView.mjs +21 -375
- package/dist/jsx/report/DoctypeReportView.mjs.map +1 -1
- package/dist/jsx/report/DoctypeReportView.native.js +23 -455
- package/dist/jsx/report/DoctypeReportView.native.js.map +1 -1
- package/dist/jsx/report/QueryReportScreen.mjs +26 -16
- package/dist/jsx/report/QueryReportScreen.mjs.map +1 -1
- package/dist/jsx/report/QueryReportScreen.native.js +33 -23
- package/dist/jsx/report/QueryReportScreen.native.js.map +1 -1
- package/dist/jsx/report/queryReportColumns.mjs +1 -103
- package/dist/jsx/report/queryReportColumns.mjs.map +1 -1
- package/dist/jsx/report/queryReportColumns.native.js +2 -129
- package/dist/jsx/report/queryReportColumns.native.js.map +1 -1
- package/dist/jsx/report/queryReportFilterBar.mjs +1 -2
- package/dist/jsx/report/queryReportFilterBar.mjs.map +1 -1
- package/dist/jsx/report/queryReportFilterBar.native.js +3 -4
- package/dist/jsx/report/queryReportFilterBar.native.js.map +1 -1
- package/dist/jsx/report/queryReportUrlState.mjs +1 -1
- package/dist/jsx/report/queryReportUrlState.native.js +2 -2
- package/dist/jsx/report/queryReportUrlState.native.js.map +1 -1
- package/dist/jsx/report/report-grid.mjs +1 -425
- package/dist/jsx/report/report-grid.mjs.map +1 -1
- package/dist/jsx/report/report-grid.native.js +1 -479
- package/dist/jsx/report/report-grid.native.js.map +1 -1
- package/dist/jsx/report/report-toolbar.mjs +2 -228
- package/dist/jsx/report/report-toolbar.mjs.map +1 -1
- package/dist/jsx/report/report-toolbar.native.js +1 -274
- package/dist/jsx/report/report-toolbar.native.js.map +1 -1
- package/dist/jsx/report/reportShapes.mjs +2 -373
- package/dist/jsx/report/reportShapes.mjs.map +1 -1
- package/dist/jsx/report/reportShapes.native.js +12 -501
- package/dist/jsx/report/reportShapes.native.js.map +1 -1
- package/dist/jsx/report/reportViewModel.mjs +2 -342
- package/dist/jsx/report/reportViewModel.mjs.map +1 -1
- package/dist/jsx/report/reportViewModel.native.js +19 -627
- package/dist/jsx/report/reportViewModel.native.js.map +1 -1
- package/dist/jsx/report/savedReports.mjs +1 -105
- package/dist/jsx/report/savedReports.mjs.map +1 -1
- package/dist/jsx/report/savedReports.native.js +1 -132
- package/dist/jsx/report/savedReports.native.js.map +1 -1
- package/dist/jsx/report/useQueryReport.mjs +2 -210
- package/dist/jsx/report/useQueryReport.mjs.map +1 -1
- package/dist/jsx/report/useQueryReport.native.js +11 -244
- package/dist/jsx/report/useQueryReport.native.js.map +1 -1
- package/dist/jsx/shared/measures.mjs +1 -3
- package/dist/jsx/shared/measures.mjs.map +1 -1
- package/dist/jsx/shared/measures.native.js +3 -5
- package/dist/jsx/shared/measures.native.js.map +1 -1
- package/dist/jsx/views/ReportView.mjs +3 -256
- package/dist/jsx/views/ReportView.mjs.map +1 -1
- package/dist/jsx/views/ReportView.native.js +3 -382
- package/dist/jsx/views/ReportView.native.js.map +1 -1
- package/dist/jsx/views/report-controls.mjs +3 -3
- package/dist/jsx/views/report-controls.mjs.map +1 -1
- package/dist/jsx/views/report-controls.native.js +6 -6
- package/dist/jsx/views/report-controls.native.js.map +1 -1
- package/package.json +12 -12
- package/src/backofficeScreens.designLaw.spec.ts +0 -5
- package/src/desk/DeskSurface.tsx +4 -7
- package/src/form/DocFormScreen.chrome.spec.ts +77 -0
- package/src/form/DocFormScreen.guest.screen.spec.tsx +175 -0
- package/src/form/DocFormScreen.rights.spec.tsx +7 -3
- package/src/form/DocFormScreen.tsx +214 -315
- package/src/i18n/en.ts +1 -0
- package/src/list/ListBulkActionsBar.rights.spec.tsx +134 -0
- package/src/list/ListBulkActionsBar.screen.spec.tsx +7 -1
- package/src/list/ListBulkActionsBar.tsx +76 -57
- package/src/list/ListFilterBar.screen.spec.tsx +14 -13
- package/src/list/sidebar-section.tsx +11 -3
- package/src/report/DoctypeReportView.tsx +28 -550
- package/src/report/QueryReportScreen.spec.tsx +75 -1
- package/src/report/QueryReportScreen.tsx +38 -22
- package/src/report/queryReportColumns.tsx +4 -123
- package/src/report/queryReportFilterBar.tsx +3 -5
- package/src/report/queryReportUrlState.ts +1 -1
- package/src/report/report-grid.tsx +4 -529
- package/src/report/report-toolbar.tsx +4 -308
- package/src/report/reportShapes.ts +25 -613
- package/src/report/reportShim.spec.ts +162 -0
- package/src/report/reportViewModel.ts +32 -605
- package/src/report/savedReports.ts +4 -161
- package/src/report/useQueryReport.ts +9 -347
- package/src/shared/measures.ts +3 -11
- package/src/views/ReportView.screen.spec.tsx +25 -20
- package/src/views/ReportView.tsx +7 -439
- package/src/views/report-controls.tsx +12 -20
- package/types/desk/DeskSurface.d.ts.map +1 -1
- package/types/form/DocFormScreen.d.ts +2 -2
- package/types/form/DocFormScreen.d.ts.map +1 -1
- package/types/i18n/en.d.ts +1 -0
- package/types/i18n/en.d.ts.map +1 -1
- package/types/list/ListBulkActionsBar.d.ts.map +1 -1
- package/types/list/sidebar-section.d.ts.map +1 -1
- package/types/report/DoctypeReportView.d.ts.map +1 -1
- package/types/report/QueryReportScreen.d.ts +1 -1
- package/types/report/QueryReportScreen.d.ts.map +1 -1
- package/types/report/queryReportColumns.d.ts +2 -11
- package/types/report/queryReportColumns.d.ts.map +1 -1
- package/types/report/queryReportFilterBar.d.ts +1 -1
- package/types/report/queryReportFilterBar.d.ts.map +1 -1
- package/types/report/queryReportUrlState.d.ts +1 -1
- package/types/report/queryReportUrlState.d.ts.map +1 -1
- package/types/report/report-grid.d.ts +2 -19
- package/types/report/report-grid.d.ts.map +1 -1
- package/types/report/report-toolbar.d.ts +2 -29
- package/types/report/report-toolbar.d.ts.map +1 -1
- package/types/report/reportShapes.d.ts +2 -115
- package/types/report/reportShapes.d.ts.map +1 -1
- package/types/report/reportViewModel.d.ts +2 -135
- package/types/report/reportViewModel.d.ts.map +1 -1
- package/types/report/savedReports.d.ts +2 -18
- package/types/report/savedReports.d.ts.map +1 -1
- package/types/report/useQueryReport.d.ts +2 -35
- package/types/report/useQueryReport.d.ts.map +1 -1
- package/types/shared/measures.d.ts +1 -2
- package/types/shared/measures.d.ts.map +1 -1
- package/types/views/ReportView.d.ts +3 -22
- package/types/views/ReportView.d.ts.map +1 -1
- package/types/views/report-controls.d.ts +2 -5
- package/types/views/report-controls.d.ts.map +1 -1
- package/src/report/report-grid.spec.tsx +0 -175
- package/src/report/report-toolbar.spec.tsx +0 -90
- package/src/report/reportShapes.spec.ts +0 -391
- package/src/report/reportViewModel.spec.ts +0 -393
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
/**
|
|
3
|
-
* The document form page — desk form parity.
|
|
3
|
+
* The document form page — desk form parity, as page chrome over FrappeForm.
|
|
4
4
|
*
|
|
5
5
|
* Composition:
|
|
6
6
|
* FormToolbar breadcrumbs / title (title_field) / status chip /
|
|
7
7
|
* Save → Submit → Cancel/Amend lifecycle / ⋯ menu
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* FrappeForm the fields: tabs, sections, columns and every rule that
|
|
9
|
+
* decides what renders, from the controller useFrappeForm
|
|
10
|
+
* returns (the same engine a bare <FrappeForm> runs)
|
|
10
11
|
* Sidebar assignments / attachments / tags / shares / likes / follow
|
|
11
12
|
* Timeline docinfo comments + versions + communications interleaved
|
|
12
13
|
* Connections linked-doctype tiles with live counts
|
|
@@ -17,24 +18,27 @@ import React from "react";
|
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import {
|
|
21
|
+
AsyncBoundary,
|
|
20
22
|
AsyncSkeleton,
|
|
21
23
|
Button,
|
|
22
24
|
ConfirmDialog,
|
|
23
25
|
EmptyState,
|
|
24
26
|
ScrollView,
|
|
25
|
-
Tabs,
|
|
26
27
|
Text,
|
|
27
28
|
XStack,
|
|
28
29
|
YStack,
|
|
29
30
|
notify,
|
|
30
|
-
type TabsItem,
|
|
31
31
|
} from "@multiplatform.one/components";
|
|
32
|
-
import { Input } from "@multiplatform.one/forms";
|
|
33
32
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
FrappeForm,
|
|
34
|
+
docinfoRights,
|
|
35
|
+
fixtureDocRights,
|
|
36
|
+
isPermissionError,
|
|
37
|
+
refusalReason,
|
|
36
38
|
useFrappeConfig,
|
|
39
|
+
useFrappeForm,
|
|
37
40
|
type DocShareGrant,
|
|
41
|
+
type FormTab,
|
|
38
42
|
} from "@multiplatform.one/frappe-ui";
|
|
39
43
|
import { useLayoutSizeClass, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
40
44
|
import { ListBulletsIcon } from "@phosphor-icons/react";
|
|
@@ -54,10 +58,8 @@ import { getListNavNeighbors, type ListNavNeighbors } from "../shared/listNavCon
|
|
|
54
58
|
import { invalidateDocinfo, useDocinfo } from "../shared/useDocinfo";
|
|
55
59
|
import { useRecentVisit } from "../shared/useRecentVisit";
|
|
56
60
|
import { useDoctypeMeta } from "../shared/useDoctypeMeta";
|
|
57
|
-
import {
|
|
61
|
+
import { groupConnectionLinks } from "./connections-data";
|
|
58
62
|
import { setCreatePrefill } from "./createPrefill";
|
|
59
|
-
import { normalizeDocPermissions } from "./docinfo";
|
|
60
|
-
import { buildPermLevelMatrix } from "./fieldPermissions";
|
|
61
63
|
import { createHttpClient, renameDoc, sendEmail } from "./formActions";
|
|
62
64
|
import {
|
|
63
65
|
EmailDialog,
|
|
@@ -66,7 +68,6 @@ import {
|
|
|
66
68
|
type JumpToFieldTarget,
|
|
67
69
|
} from "./FormDialogs";
|
|
68
70
|
import { FormErrorBoundary, wrapFieldWithErrorBoundary } from "./FormErrorBoundary";
|
|
69
|
-
import { buildCopyValues, buildFormTabs, isPromptNamed } from "./formLayout";
|
|
70
71
|
import { FormConnections } from "./FormConnections";
|
|
71
72
|
import { FormHeaderCluster } from "./FormHeaderCluster";
|
|
72
73
|
import { FormSidebarPanel } from "./FormSidebarPanel";
|
|
@@ -82,7 +83,6 @@ import {
|
|
|
82
83
|
useNavigationDirtyGuard,
|
|
83
84
|
useSaveShortcut,
|
|
84
85
|
} from "./platformHooks";
|
|
85
|
-
import { useFormEngine } from "./useFormEngine";
|
|
86
86
|
|
|
87
87
|
/** Host overrides for the form's ⋯ toolbar menu. */
|
|
88
88
|
export interface DocFormActionOverrides {
|
|
@@ -102,9 +102,9 @@ export interface DocFormScreenProps {
|
|
|
102
102
|
name: string;
|
|
103
103
|
/** Force the whole form read-only regardless of permissions. */
|
|
104
104
|
readOnly?: boolean;
|
|
105
|
-
/** Hide form sections/tabs by Section/Tab Break
|
|
105
|
+
/** Hide form sections/tabs by Section/Tab Break name or label. */
|
|
106
106
|
hiddenSections?: readonly string[];
|
|
107
|
-
/** Hide individual fields by
|
|
107
|
+
/** Hide individual fields by name. */
|
|
108
108
|
hiddenFields?: readonly string[];
|
|
109
109
|
/** Create-mode field prefill (merged over meta defaults). */
|
|
110
110
|
defaults?: Record<string, unknown>;
|
|
@@ -118,6 +118,8 @@ export interface DocFormScreenProps {
|
|
|
118
118
|
|
|
119
119
|
const CONNECTIONS_TAB = "__connections";
|
|
120
120
|
|
|
121
|
+
type DeskDoc = Record<string, unknown> & { name: string; doctype: string };
|
|
122
|
+
|
|
121
123
|
export function DocFormScreen({
|
|
122
124
|
doctype,
|
|
123
125
|
name,
|
|
@@ -153,8 +155,8 @@ export function DocFormScreen({
|
|
|
153
155
|
|
|
154
156
|
// `defaults` prop rides the SAME one-shot stash Duplicate/Amend and the
|
|
155
157
|
// `?d=` route param use (createPrefill) — stashed in a memo (not an
|
|
156
|
-
// effect) so the values land before
|
|
157
|
-
//
|
|
158
|
+
// effect) so the values land before the form's create-mode seeding runs,
|
|
159
|
+
// and keyed on content so re-renders don't re-stash consumed values.
|
|
158
160
|
const defaultsKey = defaults ? JSON.stringify(defaults) : undefined;
|
|
159
161
|
useMemo(() => {
|
|
160
162
|
if (!isCreate || !defaultsKey) return;
|
|
@@ -167,24 +169,8 @@ export function DocFormScreen({
|
|
|
167
169
|
if (!isCreate) emitEvent({ type: "doc-opened", doctype, name });
|
|
168
170
|
}, [isCreate, doctype, name, emitEvent]);
|
|
169
171
|
|
|
170
|
-
const connection = useMemo(
|
|
171
|
-
() =>
|
|
172
|
-
config?.baseURL
|
|
173
|
-
? {
|
|
174
|
-
baseURL: config.baseURL,
|
|
175
|
-
socketPort: config.socketPort,
|
|
176
|
-
siteName: config.siteName,
|
|
177
|
-
socketBaseURL: config.socketBaseURL,
|
|
178
|
-
auth: config.auth,
|
|
179
|
-
syncOptions: config.syncOptions,
|
|
180
|
-
fixtures: config.fixtures,
|
|
181
|
-
}
|
|
182
|
-
: undefined,
|
|
183
|
-
[config],
|
|
184
|
-
);
|
|
185
|
-
|
|
186
172
|
// One meta loader for the whole screen (shared getdoctype cache with the
|
|
187
|
-
// fixture branch for stories) — the
|
|
173
|
+
// fixture branch for stories) — the form, sidebar, and connections all
|
|
188
174
|
// read the same bundle.
|
|
189
175
|
const {
|
|
190
176
|
meta,
|
|
@@ -192,11 +178,14 @@ export function DocFormScreen({
|
|
|
192
178
|
error: metaError,
|
|
193
179
|
reload: refetchMeta,
|
|
194
180
|
} = useDoctypeMeta(doctype);
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
181
|
+
const metaSource = useMemo(
|
|
182
|
+
() => ({ meta, isLoading: isMetaLoading, error: metaError }),
|
|
183
|
+
[meta, isMetaLoading, metaError],
|
|
184
|
+
);
|
|
185
|
+
// Guest is refused getdoctype while it may still read the record (LC-96):
|
|
186
|
+
// FrappeForm then lays the record out read-only, and the page offers
|
|
187
|
+
// nothing that needs the meta or the docinfo Guest can't read either.
|
|
188
|
+
const metaRefused = !meta && metaError !== undefined && isPermissionError(metaError);
|
|
200
189
|
|
|
201
190
|
// Shared docinfo — one get_docinfo fetch shared with the header cluster,
|
|
202
191
|
// sidebar, and timeline panels; the screen reads the permission flags and
|
|
@@ -207,10 +196,42 @@ export function DocFormScreen({
|
|
|
207
196
|
reloadDocinfo();
|
|
208
197
|
}, [doctype, docName, reloadDocinfo]);
|
|
209
198
|
|
|
199
|
+
// Rights: the bench's per-record answer (docinfo) when it gave one, else the
|
|
200
|
+
// session's (the form resolves those). Fixture mode has no session and
|
|
201
|
+
// keeps what the doctype's rows grant.
|
|
202
|
+
const fixtureMode = Boolean(config?.fixtures);
|
|
203
|
+
const rights = useMemo(
|
|
204
|
+
() =>
|
|
205
|
+
docinfoRights(docinfo?.permissions as Record<string, unknown> | undefined) ??
|
|
206
|
+
(fixtureMode ? fixtureDocRights(meta) : undefined),
|
|
207
|
+
[docinfo?.permissions, fixtureMode, meta],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const form = useFrappeForm<DeskDoc>({
|
|
211
|
+
doctype,
|
|
212
|
+
name: docName,
|
|
213
|
+
meta: metaSource,
|
|
214
|
+
rights,
|
|
215
|
+
shares: docinfo?.shared as DocShareGrant[] | undefined,
|
|
216
|
+
readShares: false,
|
|
217
|
+
disabled: readOnly,
|
|
218
|
+
hideFields: hiddenFields,
|
|
219
|
+
hideSections: hiddenSections,
|
|
220
|
+
});
|
|
221
|
+
const connection = form.connection;
|
|
222
|
+
const currentUser = form.user ?? "";
|
|
223
|
+
const perms = form.rights;
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (metaError && !meta && !metaRefused) {
|
|
227
|
+
emitEvent({ type: "error", error: metaError, context: "form-meta" });
|
|
228
|
+
}
|
|
229
|
+
}, [metaError, meta, metaRefused, emitEvent]);
|
|
230
|
+
|
|
210
231
|
// Refresh docinfo whenever the live doc's `modified` stamp moves —
|
|
211
232
|
// an external edit usually lands with new versions/comments too.
|
|
212
233
|
const modifiedStamp =
|
|
213
|
-
typeof
|
|
234
|
+
typeof form.baseline?.modified === "string" ? form.baseline.modified : undefined;
|
|
214
235
|
const stampRef = useRef<string | undefined>(undefined);
|
|
215
236
|
useEffect(() => {
|
|
216
237
|
stampRef.current = undefined;
|
|
@@ -226,133 +247,13 @@ export function DocFormScreen({
|
|
|
226
247
|
}
|
|
227
248
|
}, [modifiedStamp, docName, doctype, reloadDocinfo]);
|
|
228
249
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// session, so it keeps the union of every role's rows.
|
|
233
|
-
// -----------------------------------------------------------------------
|
|
234
|
-
const { roles, user: sessionUser } = useUserRoles();
|
|
235
|
-
const fixtureMode = Boolean(config?.fixtures);
|
|
236
|
-
const recordOwner =
|
|
237
|
-
!isCreate && typeof engine.baseline?.owner === "string" ? engine.baseline.owner : undefined;
|
|
238
|
-
const perms = useMemo(() => {
|
|
239
|
-
const docPerms = normalizeDocPermissions(
|
|
240
|
-
docinfo?.permissions as Record<string, number> | undefined,
|
|
241
|
-
);
|
|
242
|
-
if (docPerms) {
|
|
243
|
-
return {
|
|
244
|
-
write: !!docPerms.write,
|
|
245
|
-
create: !!docPerms.create,
|
|
246
|
-
delete: !!docPerms.delete,
|
|
247
|
-
submit: !!docPerms.submit,
|
|
248
|
-
cancel: !!docPerms.cancel,
|
|
249
|
-
amend: !!docPerms.amend,
|
|
250
|
-
email: !!docPerms.email,
|
|
251
|
-
print: !!docPerms.print,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
if (!fixtureMode) {
|
|
255
|
-
const rights = sessionDocRights({
|
|
256
|
-
fixtures: false,
|
|
257
|
-
meta,
|
|
258
|
-
roles,
|
|
259
|
-
user: sessionUser,
|
|
260
|
-
owner: recordOwner,
|
|
261
|
-
shares: docinfo?.shared as DocShareGrant[] | undefined,
|
|
262
|
-
});
|
|
263
|
-
return {
|
|
264
|
-
write: rights.write,
|
|
265
|
-
create: rights.create,
|
|
266
|
-
delete: rights.delete,
|
|
267
|
-
submit: rights.submit,
|
|
268
|
-
cancel: rights.cancel,
|
|
269
|
-
amend: rights.amend,
|
|
270
|
-
email: rights.email,
|
|
271
|
-
print: rights.print,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
const rules = meta?.permissions ?? [];
|
|
275
|
-
const anyRole = (key: keyof (typeof rules)[number]) =>
|
|
276
|
-
rules.some((rule) => !rule.permlevel && !!rule[key]);
|
|
277
|
-
return {
|
|
278
|
-
write: rules.length === 0 || anyRole("write"),
|
|
279
|
-
create: rules.length === 0 || anyRole("create"),
|
|
280
|
-
delete: anyRole("delete"),
|
|
281
|
-
submit: anyRole("submit"),
|
|
282
|
-
cancel: anyRole("cancel"),
|
|
283
|
-
amend: anyRole("amend"),
|
|
284
|
-
email: rules.length === 0 || anyRole("email"),
|
|
285
|
-
print: rules.length === 0 || anyRole("print"),
|
|
286
|
-
};
|
|
287
|
-
}, [docinfo?.permissions, docinfo?.shared, meta, fixtureMode, roles, sessionUser, recordOwner]);
|
|
250
|
+
const isSubmittable = form.isSubmittable;
|
|
251
|
+
const formReadOnly = form.readOnly;
|
|
252
|
+
const jumpTargets = form.fieldTargets;
|
|
288
253
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
!!meta?.read_only ||
|
|
293
|
-
engine.wasDeleted ||
|
|
294
|
-
(!isCreate && !perms.write) ||
|
|
295
|
-
(isCreate && !perms.create);
|
|
296
|
-
|
|
297
|
-
// -----------------------------------------------------------------------
|
|
298
|
-
// Body tabs (meta + doc → SchemaForm schemas) + Connections
|
|
299
|
-
// -----------------------------------------------------------------------
|
|
300
|
-
// Permlevel matrix: session roles × meta.permissions (desk perm.js). Roles
|
|
301
|
-
// stay undefined in fixture mode → coarse permlevel>0 read-only fallback.
|
|
302
|
-
const permLevels = useMemo(
|
|
303
|
-
() => buildPermLevelMatrix(meta?.permissions, roles),
|
|
304
|
-
[meta?.permissions, roles],
|
|
305
|
-
);
|
|
306
|
-
const formTabs = useMemo(() => {
|
|
307
|
-
if (!meta) return [];
|
|
308
|
-
return buildFormTabs({
|
|
309
|
-
fields: meta.fields,
|
|
310
|
-
// depends_on evaluates against the doc plus desk's ambient context:
|
|
311
|
-
// __islocal (unsaved doc flag) and the session user behind
|
|
312
|
-
// `frappe.session.user` references.
|
|
313
|
-
doc: {
|
|
314
|
-
...engine.doc,
|
|
315
|
-
__islocal: isCreate ? 1 : 0,
|
|
316
|
-
...(engine.currentUser ? { __session_user: engine.currentUser } : {}),
|
|
317
|
-
},
|
|
318
|
-
docstatus: engine.docstatus,
|
|
319
|
-
formReadOnly,
|
|
320
|
-
isCreate,
|
|
321
|
-
detailsLabel: t("detailsTab", "Details"),
|
|
322
|
-
permLevels,
|
|
323
|
-
hiddenFields,
|
|
324
|
-
hiddenSections,
|
|
325
|
-
});
|
|
326
|
-
}, [
|
|
327
|
-
meta,
|
|
328
|
-
engine.doc,
|
|
329
|
-
engine.currentUser,
|
|
330
|
-
engine.docstatus,
|
|
331
|
-
formReadOnly,
|
|
332
|
-
isCreate,
|
|
333
|
-
permLevels,
|
|
334
|
-
hiddenFields,
|
|
335
|
-
hiddenSections,
|
|
336
|
-
t,
|
|
337
|
-
]);
|
|
338
|
-
|
|
339
|
-
const jumpTargets = useMemo<JumpToFieldTarget[]>(
|
|
340
|
-
() =>
|
|
341
|
-
formTabs.flatMap((tab) =>
|
|
342
|
-
tab.schema.fields.map((field) => ({
|
|
343
|
-
name: field.name,
|
|
344
|
-
label: field.label || field.name,
|
|
345
|
-
tabKey: tab.key,
|
|
346
|
-
tabLabel: tab.label,
|
|
347
|
-
})),
|
|
348
|
-
),
|
|
349
|
-
[formTabs],
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const [activeTab, setActiveTab] = useState<string | undefined>(undefined);
|
|
353
|
-
// Hidden link rows never render (desk parity — connections-data skips them).
|
|
354
|
-
const showConnections =
|
|
355
|
-
!isCreate && !!connection && (meta?.links?.some((link) => !link.hidden) ?? false);
|
|
254
|
+
// Connections render what FormConnections groups (connections-data skips
|
|
255
|
+
// the link rows a doctype hides, desk parity).
|
|
256
|
+
const showConnections = !isCreate && !!connection && groupConnectionLinks(meta).length > 0;
|
|
356
257
|
|
|
357
258
|
// -----------------------------------------------------------------------
|
|
358
259
|
// Navigation with dirty guard
|
|
@@ -360,18 +261,18 @@ export function DocFormScreen({
|
|
|
360
261
|
const [pendingNav, setPendingNav] = useState<string | undefined>(undefined);
|
|
361
262
|
const navigate = useCallback(
|
|
362
263
|
(href: string) => {
|
|
363
|
-
if (
|
|
264
|
+
if (form.isDirty) {
|
|
364
265
|
setPendingNav(href);
|
|
365
266
|
return;
|
|
366
267
|
}
|
|
367
268
|
navigator.navigate(href);
|
|
368
269
|
},
|
|
369
|
-
[
|
|
270
|
+
[form.isDirty, navigator],
|
|
370
271
|
);
|
|
371
|
-
useBeforeUnloadGuard(
|
|
272
|
+
useBeforeUnloadGuard(form.isDirty);
|
|
372
273
|
// Covers back navigations that skip navigate(): browser back on web,
|
|
373
274
|
// back gesture/button on native (B7).
|
|
374
|
-
const backGuard = useNavigationDirtyGuard(
|
|
275
|
+
const backGuard = useNavigationDirtyGuard(form.isDirty);
|
|
375
276
|
|
|
376
277
|
// -----------------------------------------------------------------------
|
|
377
278
|
// Actions
|
|
@@ -389,13 +290,10 @@ export function DocFormScreen({
|
|
|
389
290
|
const handleSave = useCallback(async () => {
|
|
390
291
|
// Pre-check required fields so the failing tab is surfaced — inline
|
|
391
292
|
// errors on an inactive tab would otherwise fail invisibly.
|
|
392
|
-
const validationErrors =
|
|
293
|
+
const validationErrors = form.validate();
|
|
393
294
|
const errorFields = Object.keys(validationErrors);
|
|
394
295
|
if (errorFields.length > 0) {
|
|
395
|
-
|
|
396
|
-
tab.schema.fields.some((field) => errorFields.includes(field.name)),
|
|
397
|
-
);
|
|
398
|
-
if (owner) setActiveTab(owner.key);
|
|
296
|
+
errorFields.some((field) => form.revealField(field));
|
|
399
297
|
setValidationSummary(
|
|
400
298
|
t("validationSummary", {
|
|
401
299
|
count: errorFields.length,
|
|
@@ -406,12 +304,12 @@ export function DocFormScreen({
|
|
|
406
304
|
} else {
|
|
407
305
|
setValidationSummary(undefined);
|
|
408
306
|
}
|
|
409
|
-
const savedName = await
|
|
307
|
+
const savedName = await form.save();
|
|
410
308
|
if (!savedName) return;
|
|
411
309
|
setValidationSummary(undefined);
|
|
412
310
|
// Explicit save confirmation (desk's green "Saved" toast) — the
|
|
413
311
|
// "Not Saved" badge clearing alone is too passive a success signal.
|
|
414
|
-
// Errors stay on the
|
|
312
|
+
// Errors stay on the form's inline path (errors.general).
|
|
415
313
|
notify({
|
|
416
314
|
severity: "success",
|
|
417
315
|
scope: "page",
|
|
@@ -419,36 +317,29 @@ export function DocFormScreen({
|
|
|
419
317
|
});
|
|
420
318
|
invalidateDocinfo(doctype, savedName);
|
|
421
319
|
emitEvent({ type: "doc-saved", doctype, name: savedName, isNew: isCreate });
|
|
422
|
-
onSaved?.({ ...
|
|
320
|
+
onSaved?.({ ...form.doc, name: savedName });
|
|
423
321
|
if (isCreate) {
|
|
424
322
|
navigator.replace(paths.form(doctype, savedName));
|
|
425
323
|
} else {
|
|
426
324
|
reloadDocinfo();
|
|
427
325
|
}
|
|
428
|
-
}, [
|
|
326
|
+
}, [form, isCreate, navigator, paths, doctype, reloadDocinfo, t, emitEvent, onSaved]);
|
|
429
327
|
|
|
430
328
|
// Always swallow Cmd/Ctrl+S while the form is editable (desk parity —
|
|
431
329
|
// the browser save dialog must never appear on a form page); saving
|
|
432
330
|
// itself only fires when there is something to save.
|
|
433
331
|
useSaveShortcut(!formReadOnly, () => {
|
|
434
|
-
if (
|
|
332
|
+
if (form.isDirty || isCreate) void handleSave();
|
|
435
333
|
});
|
|
436
334
|
useJumpToFieldShortcut(jumpTargets.length > 0 && !jumpOpen, () => setJumpOpen(true));
|
|
437
335
|
|
|
438
336
|
const handleAmend = useCallback(() => {
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
...buildCopyValues(meta.fields, engine.doc),
|
|
442
|
-
amended_from: docName,
|
|
443
|
-
});
|
|
444
|
-
navigator.navigate(paths.newDoc(doctype));
|
|
445
|
-
}, [meta, docName, doctype, engine.doc, navigator, paths]);
|
|
337
|
+
if (form.amend()) navigator.navigate(paths.newDoc(doctype));
|
|
338
|
+
}, [form, doctype, navigator, paths]);
|
|
446
339
|
|
|
447
340
|
const handleDuplicate = useCallback(() => {
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
navigator.navigate(paths.newDoc(doctype));
|
|
451
|
-
}, [meta, doctype, engine.doc, navigator, paths]);
|
|
341
|
+
if (form.duplicate()) navigator.navigate(paths.newDoc(doctype));
|
|
342
|
+
}, [form, doctype, navigator, paths]);
|
|
452
343
|
|
|
453
344
|
const handleRename = useCallback(
|
|
454
345
|
async (newName: string) => {
|
|
@@ -468,7 +359,7 @@ export function DocFormScreen({
|
|
|
468
359
|
// list order the user came from — DoctypeListScreen mirrors each resolved
|
|
469
360
|
// page into the shared listNavContext store (sessionStorage-backed so a
|
|
470
361
|
// reload keeps paging). No context / not in the last result (deep link,
|
|
471
|
-
// renamed doc) → no pager at all:
|
|
362
|
+
// renamed doc) → no pager at all: no arrows beat dead arrows (LC-40).
|
|
472
363
|
// -----------------------------------------------------------------------
|
|
473
364
|
const docPager = useMemo<ListNavNeighbors>(
|
|
474
365
|
() => (docName ? getListNavNeighbors(doctype, docName) : { inList: false }),
|
|
@@ -488,11 +379,11 @@ export function DocFormScreen({
|
|
|
488
379
|
);
|
|
489
380
|
|
|
490
381
|
const titleFieldValue =
|
|
491
|
-
meta?.title_field && typeof
|
|
382
|
+
meta?.title_field && typeof form.doc[meta.title_field] === "string"
|
|
492
383
|
? // Title fields can hold rich text (e.g. ToDo.description = "<p>…</p>");
|
|
493
384
|
// strip to plain text so the breadcrumb + toolbar heading don't show
|
|
494
385
|
// literal HTML tags.
|
|
495
|
-
htmlToText(
|
|
386
|
+
htmlToText(form.doc[meta.title_field] as string)
|
|
496
387
|
: undefined;
|
|
497
388
|
const title = isCreate
|
|
498
389
|
? t("newDocTitle", "New {{doctype}}", { doctype })
|
|
@@ -506,20 +397,22 @@ export function DocFormScreen({
|
|
|
506
397
|
label: t("menuReload", "Reload"),
|
|
507
398
|
icon: "reload",
|
|
508
399
|
onSelect: () => {
|
|
509
|
-
|
|
400
|
+
form.reset();
|
|
510
401
|
setValidationSummary(undefined);
|
|
511
402
|
refetchMeta();
|
|
512
403
|
invalidateDocinfo(doctype, name);
|
|
513
404
|
reloadDocinfo();
|
|
514
405
|
},
|
|
515
406
|
},
|
|
516
|
-
|
|
407
|
+
];
|
|
408
|
+
if (!metaRefused) {
|
|
409
|
+
items.push({
|
|
517
410
|
key: "duplicate",
|
|
518
411
|
label: t("menuDuplicate", "Duplicate"),
|
|
519
412
|
icon: "duplicate",
|
|
520
413
|
onSelect: handleDuplicate,
|
|
521
|
-
}
|
|
522
|
-
|
|
414
|
+
});
|
|
415
|
+
}
|
|
523
416
|
if (meta?.allow_rename && perms.write) {
|
|
524
417
|
items.push({
|
|
525
418
|
key: "rename",
|
|
@@ -558,7 +451,7 @@ export function DocFormScreen({
|
|
|
558
451
|
key: "links",
|
|
559
452
|
label: t("menuLinks", "Links"),
|
|
560
453
|
icon: "links",
|
|
561
|
-
onSelect: () => setActiveTab(CONNECTIONS_TAB),
|
|
454
|
+
onSelect: () => form.setActiveTab(CONNECTIONS_TAB),
|
|
562
455
|
});
|
|
563
456
|
}
|
|
564
457
|
if (jumpTargets.length > 0) {
|
|
@@ -606,10 +499,11 @@ export function DocFormScreen({
|
|
|
606
499
|
}, [
|
|
607
500
|
isCreate,
|
|
608
501
|
t,
|
|
609
|
-
|
|
502
|
+
form,
|
|
610
503
|
refetchMeta,
|
|
611
504
|
reloadDocinfo,
|
|
612
505
|
handleDuplicate,
|
|
506
|
+
metaRefused,
|
|
613
507
|
meta?.allow_rename,
|
|
614
508
|
perms,
|
|
615
509
|
connection,
|
|
@@ -627,8 +521,8 @@ export function DocFormScreen({
|
|
|
627
521
|
// removes built-ins by key (a host can replace "print" by hiding it and
|
|
628
522
|
// injecting its own).
|
|
629
523
|
const effectiveMenuItems = useMemo<FormToolbarMenuItem[]>(() => {
|
|
630
|
-
const
|
|
631
|
-
const items = menuItems.filter((item) => !
|
|
524
|
+
const hiddenKeys = new Set(actionOverrides?.hide ?? []);
|
|
525
|
+
const items = menuItems.filter((item) => !hiddenKeys.has(item.key));
|
|
632
526
|
const injected: BackofficeAction[] = [
|
|
633
527
|
...actionsForDoctype(actions.form, doctype),
|
|
634
528
|
...actionsForDoctype(actionOverrides?.extra, doctype),
|
|
@@ -643,11 +537,14 @@ export function DocFormScreen({
|
|
|
643
537
|
return items;
|
|
644
538
|
}, [menuItems, actions.form, actionOverrides, doctype, docName]);
|
|
645
539
|
|
|
646
|
-
const handleJumpToField = useCallback(
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
540
|
+
const handleJumpToField = useCallback(
|
|
541
|
+
(field: JumpToFieldTarget) => {
|
|
542
|
+
setJumpOpen(false);
|
|
543
|
+
form.setActiveTab(field.tabKey);
|
|
544
|
+
setPendingJump(field.name);
|
|
545
|
+
},
|
|
546
|
+
[form],
|
|
547
|
+
);
|
|
651
548
|
|
|
652
549
|
// Desk Jump-to-field: switch tab (lazyMount) then focus the LC-71 field shell.
|
|
653
550
|
useEffect(() => {
|
|
@@ -704,18 +601,53 @@ export function DocFormScreen({
|
|
|
704
601
|
);
|
|
705
602
|
}
|
|
706
603
|
|
|
707
|
-
if (metaError && !meta) {
|
|
604
|
+
if (metaError && !meta && !metaRefused) {
|
|
605
|
+
return (
|
|
606
|
+
<YStack flex={1}>
|
|
607
|
+
<AsyncBoundary
|
|
608
|
+
error={metaError}
|
|
609
|
+
errorTitle={t("metaError", "Could not load {{doctype}}", { doctype })}
|
|
610
|
+
onRetry={refetchMeta}
|
|
611
|
+
/>
|
|
612
|
+
</YStack>
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!isCreate && form.status === "error" && form.docError && isPermissionError(form.docError)) {
|
|
708
617
|
return (
|
|
709
618
|
<YStack flex={1}>
|
|
710
619
|
<EmptyState
|
|
711
|
-
|
|
712
|
-
|
|
620
|
+
testID="frappe-form-not-permitted"
|
|
621
|
+
title={t("docNotPermitted", "You can't open {{doctype}} {{name}}", { doctype, name })}
|
|
622
|
+
description={refusalReason(form.docError)}
|
|
623
|
+
action={
|
|
624
|
+
<Button icon={ListBulletsIcon} onPress={() => navigator.navigate(paths.list(doctype))}>
|
|
625
|
+
{t("backToList", "Back to {{doctype}} list", { doctype })}
|
|
626
|
+
</Button>
|
|
627
|
+
}
|
|
713
628
|
/>
|
|
714
629
|
</YStack>
|
|
715
630
|
);
|
|
716
631
|
}
|
|
717
632
|
|
|
718
|
-
if (!isCreate &&
|
|
633
|
+
if (!isCreate && form.status === "error" && form.docError) {
|
|
634
|
+
return (
|
|
635
|
+
<YStack flex={1}>
|
|
636
|
+
<EmptyState
|
|
637
|
+
intent="error"
|
|
638
|
+
title={t("docLoadError", "Could not load {{doctype}} {{name}}", { doctype, name })}
|
|
639
|
+
description={form.docError.message}
|
|
640
|
+
action={
|
|
641
|
+
<Button icon={ListBulletsIcon} onPress={() => navigator.navigate(paths.list(doctype))}>
|
|
642
|
+
{t("backToList", "Back to {{doctype}} list", { doctype })}
|
|
643
|
+
</Button>
|
|
644
|
+
}
|
|
645
|
+
/>
|
|
646
|
+
</YStack>
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!isCreate && form.status === "notFound") {
|
|
719
651
|
return (
|
|
720
652
|
<YStack flex={1}>
|
|
721
653
|
<EmptyState
|
|
@@ -737,7 +669,7 @@ export function DocFormScreen({
|
|
|
737
669
|
);
|
|
738
670
|
}
|
|
739
671
|
|
|
740
|
-
if (!isCreate &&
|
|
672
|
+
if (!isCreate && form.status === "loading") {
|
|
741
673
|
return (
|
|
742
674
|
<YStack flex={1} {...knobProps.panelPadding} testID="doc-form-skeleton">
|
|
743
675
|
<AsyncSkeleton layout="generic" />
|
|
@@ -750,25 +682,24 @@ export function DocFormScreen({
|
|
|
750
682
|
// -----------------------------------------------------------------------
|
|
751
683
|
const indicator = resolveIndicator({
|
|
752
684
|
isCreate,
|
|
753
|
-
isDirty:
|
|
754
|
-
docstatus:
|
|
685
|
+
isDirty: form.isDirty,
|
|
686
|
+
docstatus: form.docstatus,
|
|
755
687
|
isSubmittable,
|
|
756
|
-
statusValue: typeof
|
|
688
|
+
statusValue: typeof form.doc.status === "string" ? form.doc.status : undefined,
|
|
757
689
|
states: meta?.states,
|
|
758
690
|
});
|
|
759
|
-
const canSave =
|
|
760
|
-
!formReadOnly && (isCreate ? perms.create : perms.write && engine.docstatus === 0);
|
|
691
|
+
const canSave = !formReadOnly && (isCreate ? perms.create : perms.write && form.docstatus === 0);
|
|
761
692
|
const canSubmit =
|
|
762
693
|
!isCreate &&
|
|
763
694
|
isSubmittable &&
|
|
764
|
-
|
|
765
|
-
!
|
|
695
|
+
form.docstatus === 0 &&
|
|
696
|
+
!form.isDirty &&
|
|
766
697
|
perms.submit &&
|
|
767
|
-
!
|
|
698
|
+
!form.wasDeleted;
|
|
768
699
|
const canCancel =
|
|
769
|
-
!isCreate && isSubmittable &&
|
|
700
|
+
!isCreate && isSubmittable && form.docstatus === 1 && perms.cancel && !form.wasDeleted;
|
|
770
701
|
const canAmend =
|
|
771
|
-
!isCreate && isSubmittable &&
|
|
702
|
+
!isCreate && isSubmittable && form.docstatus === 2 && perms.amend && !form.wasDeleted;
|
|
772
703
|
|
|
773
704
|
const breadcrumbs = [
|
|
774
705
|
{ label: t("home", "Home"), href: paths.home() },
|
|
@@ -776,91 +707,64 @@ export function DocFormScreen({
|
|
|
776
707
|
{ label: isCreate ? t("newCrumb", "New") : title },
|
|
777
708
|
];
|
|
778
709
|
|
|
779
|
-
const promptNamed = isCreate && isPromptNamed(meta?.autoname, meta?.naming_rule);
|
|
780
710
|
// Compact (< 640): the sidebar accordion stacks between form body and
|
|
781
711
|
// timeline (LC-29 — asides stack, never hide; metadata before history).
|
|
782
712
|
const sidebarStacked = sizeClass === "compact";
|
|
783
713
|
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
>
|
|
798
|
-
<SchemaForm
|
|
799
|
-
schema={tab.schema}
|
|
800
|
-
doc={engine.doc}
|
|
801
|
-
setValue={engine.setValue}
|
|
802
|
-
errors={engine.errors}
|
|
803
|
-
disabled={formReadOnly || engine.isSaving}
|
|
804
|
-
sectionHeadingLevel={2}
|
|
805
|
-
renderFieldWrapper={wrapFieldWithErrorBoundary}
|
|
806
|
-
/>
|
|
807
|
-
</FormErrorBoundary>
|
|
808
|
-
</YStack>
|
|
809
|
-
),
|
|
810
|
-
})),
|
|
811
|
-
...(showConnections
|
|
812
|
-
? [
|
|
813
|
-
{
|
|
814
|
-
value: CONNECTIONS_TAB,
|
|
815
|
-
label: t("connectionsTab", "Connections"),
|
|
816
|
-
content: (
|
|
817
|
-
<YStack paddingTop="$3">
|
|
818
|
-
<FormConnections doctype={doctype} name={name} meta={meta} />
|
|
819
|
-
</YStack>
|
|
820
|
-
),
|
|
821
|
-
},
|
|
822
|
-
]
|
|
823
|
-
: []),
|
|
824
|
-
];
|
|
714
|
+
const connectionsTab = showConnections
|
|
715
|
+
? [
|
|
716
|
+
{
|
|
717
|
+
value: CONNECTIONS_TAB,
|
|
718
|
+
label: t("connectionsTab", "Connections"),
|
|
719
|
+
content: (
|
|
720
|
+
<YStack paddingTop="$3">
|
|
721
|
+
<FormConnections doctype={doctype} name={name} meta={meta} />
|
|
722
|
+
</YStack>
|
|
723
|
+
),
|
|
724
|
+
},
|
|
725
|
+
]
|
|
726
|
+
: [];
|
|
825
727
|
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
|
|
829
|
-
|
|
728
|
+
// Containment at two granularities: each field renders behind its own
|
|
729
|
+
// boundary (a broken field degrades to an inline error slot; siblings stay
|
|
730
|
+
// editable), and the whole tab body behind another (a crash in the form
|
|
731
|
+
// costs one tab, not the route).
|
|
732
|
+
const renderTab = (tab: FormTab, content: React.ReactNode) => (
|
|
733
|
+
<YStack {...knobProps.gapLg} paddingTop="$3">
|
|
734
|
+
<FormErrorBoundary
|
|
735
|
+
errorTitle={t("formTabRenderError", "This form tab failed to render")}
|
|
736
|
+
testID={`form-tab-error-${tab.key}`}
|
|
737
|
+
>
|
|
738
|
+
{content}
|
|
739
|
+
</FormErrorBoundary>
|
|
740
|
+
</YStack>
|
|
741
|
+
);
|
|
830
742
|
|
|
831
743
|
const formBody = (
|
|
832
|
-
// No flex here: formBody
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
<YStack minWidth={0}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
(bodyTabs[0]?.content ?? null)
|
|
852
|
-
)}
|
|
853
|
-
|
|
854
|
-
{engine.errors.general && (
|
|
855
|
-
<Text color="$red10" fontSize="$3" role="alert">
|
|
856
|
-
{engine.errors.general}
|
|
857
|
-
</Text>
|
|
858
|
-
)}
|
|
744
|
+
// No flex here: formBody lives inside COLUMN containers (both branches
|
|
745
|
+
// below), where `flex={1}` means flex-basis:0 — the body would stop
|
|
746
|
+
// contributing height and the timeline/sidebar would paint over it.
|
|
747
|
+
<YStack minWidth={0}>
|
|
748
|
+
<FrappeForm<DeskDoc>
|
|
749
|
+
form={form}
|
|
750
|
+
doctype={doctype}
|
|
751
|
+
name={docName}
|
|
752
|
+
disabled={readOnly}
|
|
753
|
+
showToolbar={false}
|
|
754
|
+
showSidebar={false}
|
|
755
|
+
showTimeline={false}
|
|
756
|
+
showConnections={false}
|
|
757
|
+
showBanners={false}
|
|
758
|
+
sectionHeadingLevel={2}
|
|
759
|
+
renderFieldWrapper={wrapFieldWithErrorBoundary}
|
|
760
|
+
renderTab={renderTab}
|
|
761
|
+
extraTabs={connectionsTab}
|
|
762
|
+
/>
|
|
859
763
|
</YStack>
|
|
860
764
|
);
|
|
861
765
|
|
|
862
766
|
const timeline =
|
|
863
|
-
capabilities.timeline.enabled && !isCreate && docName ? (
|
|
767
|
+
capabilities.timeline.enabled && !isCreate && docName && !metaRefused ? (
|
|
864
768
|
// Composed mode: the screen's shared docinfo (and its refresh) feed
|
|
865
769
|
// the timeline. Self-fetch mode subscribed the timeline to its OWN
|
|
866
770
|
// cache entry, which the save path's invalidate+reload orphaned — the
|
|
@@ -871,7 +775,7 @@ export function DocFormScreen({
|
|
|
871
775
|
name={docName}
|
|
872
776
|
docInfo={docinfo}
|
|
873
777
|
refreshDocInfo={refreshDocinfo}
|
|
874
|
-
currentUser={
|
|
778
|
+
currentUser={currentUser}
|
|
875
779
|
connection={connection}
|
|
876
780
|
/>
|
|
877
781
|
) : null;
|
|
@@ -882,9 +786,9 @@ export function DocFormScreen({
|
|
|
882
786
|
doctype={doctype}
|
|
883
787
|
name={docName}
|
|
884
788
|
meta={meta}
|
|
885
|
-
doc={
|
|
789
|
+
doc={form.doc}
|
|
886
790
|
docinfo={docinfo}
|
|
887
|
-
currentUser={
|
|
791
|
+
currentUser={currentUser}
|
|
888
792
|
onRefresh={refreshDocinfo}
|
|
889
793
|
stacked={sidebarStacked}
|
|
890
794
|
/>
|
|
@@ -897,14 +801,14 @@ export function DocFormScreen({
|
|
|
897
801
|
title={title}
|
|
898
802
|
indicator={indicator}
|
|
899
803
|
showIndicator={!!meta}
|
|
900
|
-
isSaving={
|
|
804
|
+
isSaving={form.isSubmitting}
|
|
901
805
|
cluster={
|
|
902
|
-
!isCreate && docName ? (
|
|
806
|
+
!isCreate && docName && !metaRefused ? (
|
|
903
807
|
<FormHeaderCluster
|
|
904
808
|
doctype={doctype}
|
|
905
809
|
name={docName}
|
|
906
810
|
docinfo={docinfo}
|
|
907
|
-
currentUser={
|
|
811
|
+
currentUser={currentUser}
|
|
908
812
|
onRefresh={refreshDocinfo}
|
|
909
813
|
/>
|
|
910
814
|
) : undefined
|
|
@@ -944,9 +848,9 @@ export function DocFormScreen({
|
|
|
944
848
|
})
|
|
945
849
|
: toolbar}
|
|
946
850
|
|
|
947
|
-
{(
|
|
851
|
+
{(form.wasDeleted || validationSummary || form.conflict) && (
|
|
948
852
|
<YStack paddingHorizontal="$3">
|
|
949
|
-
{
|
|
853
|
+
{form.wasDeleted && (
|
|
950
854
|
<XStack
|
|
951
855
|
role="alert"
|
|
952
856
|
{...knobProps.containerRadius}
|
|
@@ -962,7 +866,7 @@ export function DocFormScreen({
|
|
|
962
866
|
</XStack>
|
|
963
867
|
)}
|
|
964
868
|
|
|
965
|
-
{validationSummary && !
|
|
869
|
+
{validationSummary && !form.wasDeleted && (
|
|
966
870
|
<XStack
|
|
967
871
|
role="alert"
|
|
968
872
|
{...knobProps.containerRadius}
|
|
@@ -976,7 +880,7 @@ export function DocFormScreen({
|
|
|
976
880
|
</XStack>
|
|
977
881
|
)}
|
|
978
882
|
|
|
979
|
-
{
|
|
883
|
+
{form.conflict && !form.wasDeleted && (
|
|
980
884
|
<XStack
|
|
981
885
|
role="status"
|
|
982
886
|
alignItems="center"
|
|
@@ -989,12 +893,7 @@ export function DocFormScreen({
|
|
|
989
893
|
<Text color="$orange11" fontSize="$3" flex={1}>
|
|
990
894
|
{t("conflictBanner", "This document changed on the server while you were editing.")}
|
|
991
895
|
</Text>
|
|
992
|
-
<Button
|
|
993
|
-
size="$2"
|
|
994
|
-
outlined
|
|
995
|
-
aria-label={t("reloadDoc", "Reload")}
|
|
996
|
-
onPress={engine.reload}
|
|
997
|
-
>
|
|
896
|
+
<Button size="$2" outlined aria-label={t("reloadDoc", "Reload")} onPress={form.reset}>
|
|
998
897
|
{t("reloadDoc", "Reload")}
|
|
999
898
|
</Button>
|
|
1000
899
|
</XStack>
|
|
@@ -1037,7 +936,7 @@ export function DocFormScreen({
|
|
|
1037
936
|
warning
|
|
1038
937
|
onConfirm={() => {
|
|
1039
938
|
setConfirmAction(undefined);
|
|
1040
|
-
void
|
|
939
|
+
void form.submitDocument().then((ok) => {
|
|
1041
940
|
if (ok) {
|
|
1042
941
|
notify({
|
|
1043
942
|
severity: "success",
|
|
@@ -1061,7 +960,7 @@ export function DocFormScreen({
|
|
|
1061
960
|
destructive
|
|
1062
961
|
onConfirm={() => {
|
|
1063
962
|
setConfirmAction(undefined);
|
|
1064
|
-
void
|
|
963
|
+
void form.cancelDocument().then((ok) => {
|
|
1065
964
|
if (ok) {
|
|
1066
965
|
notify({
|
|
1067
966
|
severity: "success",
|
|
@@ -1085,7 +984,7 @@ export function DocFormScreen({
|
|
|
1085
984
|
destructive
|
|
1086
985
|
onConfirm={() => {
|
|
1087
986
|
setConfirmAction(undefined);
|
|
1088
|
-
void
|
|
987
|
+
void form.remove().then((ok) => {
|
|
1089
988
|
if (ok) {
|
|
1090
989
|
invalidateDocinfo(doctype, name);
|
|
1091
990
|
emitEvent({ type: "doc-deleted", doctype, name });
|