@multiplatform.one/frappe-ui 7.22.0 → 7.23.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 +14 -0
- package/dist/cjs/FrappeForm.cjs +96 -52
- package/dist/cjs/FrappeForm.native.js +103 -52
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeFormActions.cjs +161 -0
- package/dist/cjs/FrappeFormActions.native.js +167 -0
- package/dist/cjs/FrappeFormActions.native.js.map +1 -0
- package/dist/cjs/FrappeTable.cjs +42 -13
- package/dist/cjs/FrappeTable.native.js +55 -19
- package/dist/cjs/FrappeTable.native.js.map +1 -1
- package/dist/cjs/desk/DeskFormPage.cjs +2 -1
- package/dist/cjs/desk/DeskFormPage.native.js +3 -0
- package/dist/cjs/desk/DeskFormPage.native.js.map +1 -1
- package/dist/cjs/filterFields.cjs +277 -34
- package/dist/cjs/filterFields.native.js +311 -36
- package/dist/cjs/filterFields.native.js.map +1 -1
- package/dist/cjs/formEngine/docLifecycle.cjs +98 -0
- package/dist/cjs/formEngine/docLifecycle.native.js +131 -0
- package/dist/cjs/formEngine/docLifecycle.native.js.map +1 -0
- package/dist/cjs/formEngine/useAmendedAs.cjs +48 -0
- package/dist/cjs/formEngine/useAmendedAs.native.js +60 -0
- package/dist/cjs/formEngine/useAmendedAs.native.js.map +1 -0
- package/dist/cjs/formEngine/useDocRights.cjs +2 -2
- package/dist/cjs/formEngine/useDocRights.native.js +2 -2
- package/dist/cjs/formEngine/useDocRights.native.js.map +1 -1
- package/dist/cjs/formEngine/useFormEngine.cjs +1 -0
- package/dist/cjs/formEngine/useFormEngine.native.js +1 -0
- package/dist/cjs/formEngine/useFormEngine.native.js.map +1 -1
- package/dist/cjs/index.cjs +30 -1
- package/dist/cjs/index.native.js +30 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/list/listColumns.cjs +397 -0
- package/dist/cjs/list/listColumns.native.js +447 -0
- package/dist/cjs/list/listColumns.native.js.map +1 -0
- package/dist/cjs/list/listFields.cjs +50 -0
- package/dist/cjs/list/listFields.native.js +79 -0
- package/dist/cjs/list/listFields.native.js.map +1 -0
- package/dist/cjs/list/listFormat.cjs +68 -0
- package/dist/cjs/list/listFormat.native.js +74 -0
- package/dist/cjs/list/listFormat.native.js.map +1 -0
- package/dist/cjs/list/statusColors.cjs +102 -0
- package/dist/cjs/list/statusColors.native.js +109 -0
- package/dist/cjs/list/statusColors.native.js.map +1 -0
- package/dist/esm/FrappeForm.mjs +97 -53
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +104 -53
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeFormActions.mjs +136 -0
- package/dist/esm/FrappeFormActions.mjs.map +1 -0
- package/dist/esm/FrappeFormActions.native.js +139 -0
- package/dist/esm/FrappeFormActions.native.js.map +1 -0
- package/dist/esm/FrappeTable.mjs +42 -13
- package/dist/esm/FrappeTable.mjs.map +1 -1
- package/dist/esm/FrappeTable.native.js +55 -19
- package/dist/esm/FrappeTable.native.js.map +1 -1
- package/dist/esm/desk/DeskFormPage.mjs +2 -1
- package/dist/esm/desk/DeskFormPage.mjs.map +1 -1
- package/dist/esm/desk/DeskFormPage.native.js +3 -0
- package/dist/esm/desk/DeskFormPage.native.js.map +1 -1
- package/dist/esm/filterFields.mjs +263 -34
- package/dist/esm/filterFields.mjs.map +1 -1
- package/dist/esm/filterFields.native.js +297 -36
- package/dist/esm/filterFields.native.js.map +1 -1
- package/dist/esm/formEngine/docLifecycle.mjs +68 -0
- package/dist/esm/formEngine/docLifecycle.mjs.map +1 -0
- package/dist/esm/formEngine/docLifecycle.native.js +98 -0
- package/dist/esm/formEngine/docLifecycle.native.js.map +1 -0
- package/dist/esm/formEngine/useAmendedAs.mjs +23 -0
- package/dist/esm/formEngine/useAmendedAs.mjs.map +1 -0
- package/dist/esm/formEngine/useAmendedAs.native.js +32 -0
- package/dist/esm/formEngine/useAmendedAs.native.js.map +1 -0
- package/dist/esm/formEngine/useDocRights.mjs +2 -2
- package/dist/esm/formEngine/useDocRights.mjs.map +1 -1
- package/dist/esm/formEngine/useDocRights.native.js +2 -2
- package/dist/esm/formEngine/useDocRights.native.js.map +1 -1
- package/dist/esm/formEngine/useFormEngine.mjs +1 -1
- package/dist/esm/formEngine/useFormEngine.mjs.map +1 -1
- package/dist/esm/formEngine/useFormEngine.native.js +1 -1
- package/dist/esm/formEngine/useFormEngine.native.js.map +1 -1
- package/dist/esm/index.mjs +6 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +6 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/list/listColumns.mjs +372 -0
- package/dist/esm/list/listColumns.mjs.map +1 -0
- package/dist/esm/list/listColumns.native.js +419 -0
- package/dist/esm/list/listColumns.native.js.map +1 -0
- package/dist/esm/list/listFields.mjs +23 -0
- package/dist/esm/list/listFields.mjs.map +1 -0
- package/dist/esm/list/listFields.native.js +49 -0
- package/dist/esm/list/listFields.native.js.map +1 -0
- package/dist/esm/list/listFormat.mjs +38 -0
- package/dist/esm/list/listFormat.mjs.map +1 -0
- package/dist/esm/list/listFormat.native.js +41 -0
- package/dist/esm/list/listFormat.native.js.map +1 -0
- package/dist/esm/list/statusColors.mjs +77 -0
- package/dist/esm/list/statusColors.mjs.map +1 -0
- package/dist/esm/list/statusColors.native.js +81 -0
- package/dist/esm/list/statusColors.native.js.map +1 -0
- package/dist/jsx/FrappeForm.mjs +97 -53
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +103 -52
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeFormActions.mjs +136 -0
- package/dist/jsx/FrappeFormActions.mjs.map +1 -0
- package/dist/jsx/FrappeFormActions.native.js +167 -0
- package/dist/jsx/FrappeFormActions.native.js.map +1 -0
- package/dist/jsx/FrappeTable.mjs +42 -13
- package/dist/jsx/FrappeTable.mjs.map +1 -1
- package/dist/jsx/FrappeTable.native.js +55 -19
- package/dist/jsx/FrappeTable.native.js.map +1 -1
- package/dist/jsx/desk/DeskFormPage.mjs +2 -1
- package/dist/jsx/desk/DeskFormPage.mjs.map +1 -1
- package/dist/jsx/desk/DeskFormPage.native.js +3 -0
- package/dist/jsx/desk/DeskFormPage.native.js.map +1 -1
- package/dist/jsx/filterFields.mjs +263 -34
- package/dist/jsx/filterFields.mjs.map +1 -1
- package/dist/jsx/filterFields.native.js +311 -36
- package/dist/jsx/filterFields.native.js.map +1 -1
- package/dist/jsx/formEngine/docLifecycle.mjs +68 -0
- package/dist/jsx/formEngine/docLifecycle.mjs.map +1 -0
- package/dist/jsx/formEngine/docLifecycle.native.js +131 -0
- package/dist/jsx/formEngine/docLifecycle.native.js.map +1 -0
- package/dist/jsx/formEngine/useAmendedAs.mjs +23 -0
- package/dist/jsx/formEngine/useAmendedAs.mjs.map +1 -0
- package/dist/jsx/formEngine/useAmendedAs.native.js +60 -0
- package/dist/jsx/formEngine/useAmendedAs.native.js.map +1 -0
- package/dist/jsx/formEngine/useDocRights.mjs +2 -2
- package/dist/jsx/formEngine/useDocRights.mjs.map +1 -1
- package/dist/jsx/formEngine/useDocRights.native.js +2 -2
- package/dist/jsx/formEngine/useDocRights.native.js.map +1 -1
- package/dist/jsx/formEngine/useFormEngine.mjs +1 -1
- package/dist/jsx/formEngine/useFormEngine.mjs.map +1 -1
- package/dist/jsx/formEngine/useFormEngine.native.js +1 -0
- package/dist/jsx/formEngine/useFormEngine.native.js.map +1 -1
- package/dist/jsx/index.js +6 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +6 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +30 -1
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/list/listColumns.mjs +372 -0
- package/dist/jsx/list/listColumns.mjs.map +1 -0
- package/dist/jsx/list/listColumns.native.js +447 -0
- package/dist/jsx/list/listColumns.native.js.map +1 -0
- package/dist/jsx/list/listFields.mjs +23 -0
- package/dist/jsx/list/listFields.mjs.map +1 -0
- package/dist/jsx/list/listFields.native.js +79 -0
- package/dist/jsx/list/listFields.native.js.map +1 -0
- package/dist/jsx/list/listFormat.mjs +38 -0
- package/dist/jsx/list/listFormat.mjs.map +1 -0
- package/dist/jsx/list/listFormat.native.js +74 -0
- package/dist/jsx/list/listFormat.native.js.map +1 -0
- package/dist/jsx/list/statusColors.mjs +77 -0
- package/dist/jsx/list/statusColors.mjs.map +1 -0
- package/dist/jsx/list/statusColors.native.js +109 -0
- package/dist/jsx/list/statusColors.native.js.map +1 -0
- package/package.json +22 -16
- package/src/FrappeForm.deskSemantics.spec.tsx +1 -1
- package/src/FrappeForm.lifecycle.spec.tsx +463 -0
- package/src/FrappeForm.lifecycle.stories.tsx +84 -0
- package/src/FrappeForm.tsx +124 -84
- package/src/FrappeFormActions.tsx +216 -0
- package/src/FrappeTable.defaultColumns.spec.tsx +229 -0
- package/src/FrappeTable.tsx +79 -32
- package/src/desk/DeskFormPage.tsx +1 -0
- package/src/filterFields.spec.ts +57 -2
- package/src/filterFields.ts +365 -38
- package/src/formEngine/docLifecycle.spec.ts +195 -0
- package/src/formEngine/docLifecycle.ts +148 -0
- package/src/formEngine/useAmendedAs.ts +38 -0
- package/src/formEngine/useDocRights.ts +11 -3
- package/src/formEngine/useFormEngine.ts +1 -1
- package/src/frappeViews.designLaw.spec.ts +6 -0
- package/src/index.ts +47 -1
- package/src/list/listColumns.tsx +488 -0
- package/src/list/listFields.spec.ts +147 -0
- package/src/list/listFields.ts +68 -0
- package/src/list/listFormat.spec.ts +151 -0
- package/src/list/listFormat.ts +80 -0
- package/src/list/seam.spec.ts +116 -0
- package/src/list/statusColors.spec.ts +100 -0
- package/src/list/statusColors.ts +94 -0
- package/types/FrappeForm.d.ts +13 -3
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeFormActions.d.ts +36 -0
- package/types/FrappeFormActions.d.ts.map +1 -0
- package/types/FrappeTable.d.ts +11 -5
- package/types/FrappeTable.d.ts.map +1 -1
- package/types/desk/DeskFormPage.d.ts.map +1 -1
- package/types/filterFields.d.ts +81 -10
- package/types/filterFields.d.ts.map +1 -1
- package/types/formEngine/docLifecycle.d.ts +59 -0
- package/types/formEngine/docLifecycle.d.ts.map +1 -0
- package/types/formEngine/useAmendedAs.d.ts +9 -0
- package/types/formEngine/useAmendedAs.d.ts.map +1 -0
- package/types/formEngine/useDocRights.d.ts +1 -1
- package/types/formEngine/useDocRights.d.ts.map +1 -1
- package/types/formEngine/useFormEngine.d.ts +1 -0
- package/types/formEngine/useFormEngine.d.ts.map +1 -1
- package/types/index.d.ts +5 -1
- package/types/index.d.ts.map +1 -1
- package/types/list/listColumns.d.ts +41 -0
- package/types/list/listColumns.d.ts.map +1 -0
- package/types/list/listFields.d.ts +16 -0
- package/types/list/listFields.d.ts.map +1 -0
- package/types/list/listFormat.d.ts +30 -0
- package/types/list/listFormat.d.ts.map +1 -0
- package/types/list/statusColors.d.ts +14 -0
- package/types/list/statusColors.d.ts.map +1 -0
package/src/FrappeForm.tsx
CHANGED
|
@@ -21,13 +21,7 @@ import { SchemaForm, type FieldOverride } from "./schemaForm";
|
|
|
21
21
|
import type { HeadingLevel } from "./fields/Heading";
|
|
22
22
|
import { FormSidebar, type SidebarSection, type SidebarMetadataEntry } from "./FormSidebar";
|
|
23
23
|
import { eq } from "@tanstack/db";
|
|
24
|
-
import {
|
|
25
|
-
ArrowLeftIcon,
|
|
26
|
-
FloppyDiskIcon,
|
|
27
|
-
PencilSimpleIcon,
|
|
28
|
-
TrashIcon,
|
|
29
|
-
XIcon,
|
|
30
|
-
} from "@phosphor-icons/react";
|
|
24
|
+
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
|
31
25
|
import type React from "react";
|
|
32
26
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
33
27
|
import type { TFunction } from "i18next";
|
|
@@ -43,7 +37,19 @@ import { applyFetchedValues, buildFetchMap } from "./formEngine/fetchFrom";
|
|
|
43
37
|
import { buildPermLevelMatrix } from "./formEngine/fieldPermissions";
|
|
44
38
|
import { createHttpClient, validateLink } from "./formEngine/formActions";
|
|
45
39
|
import { buildFormTabs, isPromptNamed, type FormTab } from "./formEngine/formLayout";
|
|
40
|
+
import { setCreatePrefill, takeCreatePrefill } from "./formEngine/createPrefill";
|
|
41
|
+
import {
|
|
42
|
+
buildAmendValues,
|
|
43
|
+
canDeleteDoc,
|
|
44
|
+
canEditDoc,
|
|
45
|
+
isAmendable,
|
|
46
|
+
resolveDocAction,
|
|
47
|
+
resolveDocIndicator,
|
|
48
|
+
} from "./formEngine/docLifecycle";
|
|
49
|
+
import { useAmendedAs } from "./formEngine/useAmendedAs";
|
|
46
50
|
import { useDocRights } from "./formEngine/useDocRights";
|
|
51
|
+
import { extractServerError } from "./formEngine/useFormEngine";
|
|
52
|
+
import { FrappeFormActions } from "./FrappeFormActions";
|
|
47
53
|
import { connectionFields, connectionsFromMeta, type FrappeConnection } from "./frappeConnections";
|
|
48
54
|
import { FrappeFormConnections } from "./FrappeFormConnections";
|
|
49
55
|
import { FrappeRowsReader, type FrappeRowsSnapshot } from "./FrappeRowsReader";
|
|
@@ -132,9 +138,18 @@ export interface FrappeFormProps<TDoc extends { name: string; doctype: string }>
|
|
|
132
138
|
onSave?: (doc: Partial<TDoc>) => void;
|
|
133
139
|
onCancel?: () => void;
|
|
134
140
|
onDelete?: (doc: Partial<TDoc>) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Opens the amended draft of a cancelled document (desk Amend). The draft
|
|
143
|
+
* carries `amended_from` and is also handed to the doctype's next
|
|
144
|
+
* create-mode FrappeForm, so navigating to one is enough. Absent, a
|
|
145
|
+
* cancelled document offers no Amend.
|
|
146
|
+
*/
|
|
147
|
+
onAmend?: (draft: Partial<TDoc>) => void;
|
|
135
148
|
/**
|
|
136
149
|
* Given, the form is a read-only view of the record: its toolbar offers
|
|
137
|
-
* Edit (this callback)
|
|
150
|
+
* Edit (this callback) instead of Save and Cancel, beside Delete and the
|
|
151
|
+
* document's Submit, Cancel or Amend. A document with nothing left to
|
|
152
|
+
* edit (cancelled, or submitted without allow_on_submit fields) shows no Edit.
|
|
138
153
|
*/
|
|
139
154
|
onEdit?: () => void;
|
|
140
155
|
/**
|
|
@@ -142,8 +157,9 @@ export interface FrappeFormProps<TDoc extends { name: string; doctype: string }>
|
|
|
142
157
|
* the doctype's record page: it reads the record from the path through
|
|
143
158
|
* the FrappeProvider's navigation (`route/new` creates, `route/<name>`
|
|
144
159
|
* views, `route/<name>/edit` edits), carries the detail page template,
|
|
145
|
-
* and after a save, cancel, edit or delete moves to the matching page
|
|
146
|
-
*
|
|
160
|
+
* and after a save, cancel, edit or delete moves to the matching page;
|
|
161
|
+
* Amend opens the amended draft on `route/new`. Callbacks given alongside
|
|
162
|
+
* it replace those moves.
|
|
147
163
|
*/
|
|
148
164
|
route?: string;
|
|
149
165
|
showToolbar?: boolean;
|
|
@@ -386,6 +402,7 @@ function FrappeFormRoute<TDoc extends { name: string; doctype: string }>({
|
|
|
386
402
|
onCancel,
|
|
387
403
|
onDelete,
|
|
388
404
|
onEdit,
|
|
405
|
+
onAmend,
|
|
389
406
|
...props
|
|
390
407
|
}: Omit<FrappeFormProps<TDoc>, "route"> & { route: string }) {
|
|
391
408
|
const router = useFrappeRouter();
|
|
@@ -419,6 +436,7 @@ function FrappeFormRoute<TDoc extends { name: string; doctype: string }>({
|
|
|
419
436
|
}
|
|
420
437
|
onCancel={onCancel ?? (() => go(name ? { mode: "view", name } : { mode: "list" }))}
|
|
421
438
|
onDelete={onDelete ?? (() => go({ mode: "list" }))}
|
|
439
|
+
onAmend={onAmend ?? (() => go({ mode: "create" }))}
|
|
422
440
|
/>
|
|
423
441
|
);
|
|
424
442
|
}
|
|
@@ -440,6 +458,7 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
440
458
|
onSave,
|
|
441
459
|
onCancel,
|
|
442
460
|
onDelete,
|
|
461
|
+
onAmend,
|
|
443
462
|
onEdit,
|
|
444
463
|
showToolbar = true,
|
|
445
464
|
showSidebar = true,
|
|
@@ -477,6 +496,9 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
477
496
|
localDocRef.current = localDoc;
|
|
478
497
|
const baselineRef = useRef(baselineDoc);
|
|
479
498
|
baselineRef.current = baselineDoc;
|
|
499
|
+
// Taken once per create session: effects re-run (StrictMode) must not
|
|
500
|
+
// lose a draft the one-shot stash already handed over.
|
|
501
|
+
const createDraftRef = useRef<{ doctype: string; values?: Record<string, unknown> }>(undefined);
|
|
480
502
|
|
|
481
503
|
const finalBaseURL = baseURL ?? config?.baseURL;
|
|
482
504
|
const finalAuth = auth ?? config?.auth;
|
|
@@ -516,12 +538,18 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
516
538
|
error: metaError,
|
|
517
539
|
} = useDocTypeMeta(frappeConfig, doctype);
|
|
518
540
|
|
|
541
|
+
const isSubmittable = !!meta?.is_submittable;
|
|
519
542
|
const isCreate = !name;
|
|
520
543
|
const docstatus = Number((baselineDoc as Record<string, unknown> | undefined)?.docstatus ?? 0);
|
|
521
544
|
const layoutFields = useMemo(() => {
|
|
522
545
|
if (!meta) return undefined;
|
|
523
546
|
return sections ? sectionsToLayoutFields(sections) : meta.fields;
|
|
524
547
|
}, [meta, sections]);
|
|
548
|
+
const amendedAs = useAmendedAs(
|
|
549
|
+
isSubmittable && docstatus === 2 ? frappeConfig : undefined,
|
|
550
|
+
doctype,
|
|
551
|
+
name,
|
|
552
|
+
);
|
|
525
553
|
// Meta is permission-gated (Guest gets 403) while the record may still be
|
|
526
554
|
// readable: the form then lays the record's own fields out, read-only.
|
|
527
555
|
const readsWithoutMeta = !children && !meta && Boolean(metaError) && Boolean(name);
|
|
@@ -649,6 +677,7 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
649
677
|
// Reset per-document state when the target document changes.
|
|
650
678
|
useEffect(() => {
|
|
651
679
|
everLoadedRef.current = false;
|
|
680
|
+
if (name) createDraftRef.current = undefined;
|
|
652
681
|
setWasDeleted(false);
|
|
653
682
|
setConflictFields([]);
|
|
654
683
|
setDocError(null);
|
|
@@ -746,7 +775,10 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
746
775
|
// the server (doc_deleted event, CDC tombstone, or 404 eviction).
|
|
747
776
|
setWasDeleted(true);
|
|
748
777
|
} else if (!name) {
|
|
749
|
-
|
|
778
|
+
if (createDraftRef.current?.doctype !== doctype) {
|
|
779
|
+
createDraftRef.current = { doctype, values: takeCreatePrefill(doctype) };
|
|
780
|
+
}
|
|
781
|
+
setLocalDoc({ ...createDraftRef.current.values, doctype } as Partial<TDoc>);
|
|
750
782
|
}
|
|
751
783
|
}, [liveDoc?.data, liveDoc?.isLoading, doctype, name]);
|
|
752
784
|
|
|
@@ -930,6 +962,46 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
930
962
|
}
|
|
931
963
|
};
|
|
932
964
|
|
|
965
|
+
// Submit and Cancel write docstatus through the collection that owns the
|
|
966
|
+
// doc: the server's save runs the transition (0 → 1 submits, 1 → 2
|
|
967
|
+
// cancels) under the same OCC as any other edit.
|
|
968
|
+
const transitionDocstatus = async (to: 1 | 2, question: string, failure: string) => {
|
|
969
|
+
if (!docCollection || !name || wasDeleted || !rights[to === 1 ? "submit" : "cancel"]) return;
|
|
970
|
+
if (to === 1) {
|
|
971
|
+
const requiredErrors = validateRequiredFields();
|
|
972
|
+
if (Object.keys(requiredErrors).length > 0) {
|
|
973
|
+
setErrors(requiredErrors);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (!(await dialog.confirm(question))) return;
|
|
978
|
+
setIsSubmitting(true);
|
|
979
|
+
setErrors({});
|
|
980
|
+
try {
|
|
981
|
+
const tx = docCollection.update(name, (draft) => {
|
|
982
|
+
(draft as Record<string, unknown>).docstatus = to;
|
|
983
|
+
});
|
|
984
|
+
await tx.isPersisted.promise;
|
|
985
|
+
} catch (err) {
|
|
986
|
+
setErrors({ general: extractServerError(err, failure) });
|
|
987
|
+
} finally {
|
|
988
|
+
setIsSubmitting(false);
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const handleSubmitDocument = () =>
|
|
993
|
+
transitionDocstatus(1, t("Permanently Submit {{name}}?", { name }), t("Failed to submit"));
|
|
994
|
+
|
|
995
|
+
const handleCancelDocument = () =>
|
|
996
|
+
transitionDocstatus(2, t("Permanently Cancel {{name}}?", { name }), t("Failed to cancel"));
|
|
997
|
+
|
|
998
|
+
const handleAmend = () => {
|
|
999
|
+
if (!meta || !name || !onAmend || !rights.amend) return;
|
|
1000
|
+
const draft = buildAmendValues(meta.fields, localDoc as Record<string, unknown>, name);
|
|
1001
|
+
setCreatePrefill(doctype, draft);
|
|
1002
|
+
onAmend(draft as Partial<TDoc>);
|
|
1003
|
+
};
|
|
1004
|
+
|
|
933
1005
|
const titleField = meta?.title_field;
|
|
934
1006
|
const titleValue = titleField ? (localDoc as Record<string, unknown>)[titleField] : undefined;
|
|
935
1007
|
const pageTitle = isCreate ? t("New {{doctype}}", { doctype }) : String(titleValue || name);
|
|
@@ -1043,15 +1115,35 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
1043
1115
|
const effectiveDisabled = disabled || wasDeleted;
|
|
1044
1116
|
const readOnlyView = Boolean(onEdit) || readsWithoutMeta;
|
|
1045
1117
|
const fieldsDisabled = effectiveDisabled || readOnlyView || !canWriteRecord;
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1118
|
+
const indicator = resolveDocIndicator({
|
|
1119
|
+
docstatus,
|
|
1120
|
+
isCreate,
|
|
1121
|
+
isDirty: hasUnsavedChanges && !onEdit,
|
|
1122
|
+
isSubmittable,
|
|
1123
|
+
});
|
|
1124
|
+
const docAction = resolveDocAction({
|
|
1125
|
+
docstatus,
|
|
1126
|
+
isCreate,
|
|
1127
|
+
isDirty: hasUnsavedChanges,
|
|
1128
|
+
isSubmittable,
|
|
1129
|
+
perms: {
|
|
1130
|
+
write: canWriteRecord,
|
|
1131
|
+
submit: rights.submit,
|
|
1132
|
+
cancel: rights.cancel,
|
|
1133
|
+
amend: rights.amend,
|
|
1134
|
+
},
|
|
1135
|
+
isReadOnlyView: Boolean(onEdit),
|
|
1136
|
+
});
|
|
1137
|
+
const showDelete = Boolean(
|
|
1138
|
+
name && onDelete && !wasDeleted && rights.delete && canDeleteDoc(docstatus),
|
|
1139
|
+
);
|
|
1140
|
+
const showEdit = Boolean(onEdit && rights.write && canEditDoc(docstatus, meta));
|
|
1141
|
+
const showAmend = Boolean(onAmend && isAmendable(meta));
|
|
1142
|
+
const showClose = !onEdit && Boolean(onCancel);
|
|
1051
1143
|
const toolbarShown =
|
|
1052
1144
|
showToolbar &&
|
|
1053
1145
|
!readsWithoutMeta &&
|
|
1054
|
-
(
|
|
1146
|
+
Boolean(indicator || docAction || showDelete || showEdit || showClose);
|
|
1055
1147
|
const shownTabs = formTabs ?? recordTabs;
|
|
1056
1148
|
const imageFieldName = meta?.image_field;
|
|
1057
1149
|
const sidebarImageUrl = imageFieldName
|
|
@@ -1227,73 +1319,21 @@ function FrappeFormBody<TDoc extends { name: string; doctype: string }>({
|
|
|
1227
1319
|
)}
|
|
1228
1320
|
|
|
1229
1321
|
{toolbarShown && (
|
|
1230
|
-
<
|
|
1231
|
-
{
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
{
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
{
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
error
|
|
1246
|
-
icon={TrashIcon}
|
|
1247
|
-
loading={isSubmitting}
|
|
1248
|
-
disabled={effectiveDisabled}
|
|
1249
|
-
disabledReason={t("Form is read-only")}
|
|
1250
|
-
disabledReasonPlacement="inline"
|
|
1251
|
-
onPress={handleDelete}
|
|
1252
|
-
>
|
|
1253
|
-
{t("Delete")}
|
|
1254
|
-
</Button>
|
|
1255
|
-
)}
|
|
1256
|
-
|
|
1257
|
-
{showEdit && (
|
|
1258
|
-
<Button
|
|
1259
|
-
accent
|
|
1260
|
-
icon={PencilSimpleIcon}
|
|
1261
|
-
disabled={effectiveDisabled}
|
|
1262
|
-
disabledReason={t("Form is read-only")}
|
|
1263
|
-
disabledReasonPlacement="inline"
|
|
1264
|
-
onPress={onEdit}
|
|
1265
|
-
>
|
|
1266
|
-
{t("Edit")}
|
|
1267
|
-
</Button>
|
|
1268
|
-
)}
|
|
1269
|
-
|
|
1270
|
-
{showCancel && (
|
|
1271
|
-
<Button
|
|
1272
|
-
outlined
|
|
1273
|
-
icon={XIcon}
|
|
1274
|
-
disabled={disabled}
|
|
1275
|
-
disabledReason={t("Form is read-only")}
|
|
1276
|
-
disabledReasonPlacement="inline"
|
|
1277
|
-
onPress={handleCancel}
|
|
1278
|
-
>
|
|
1279
|
-
{t("Cancel")}
|
|
1280
|
-
</Button>
|
|
1281
|
-
)}
|
|
1282
|
-
|
|
1283
|
-
{showSave && (
|
|
1284
|
-
<Button
|
|
1285
|
-
accent
|
|
1286
|
-
icon={FloppyDiskIcon}
|
|
1287
|
-
loading={isSubmitting}
|
|
1288
|
-
disabled={effectiveDisabled}
|
|
1289
|
-
disabledReason={t("Form is read-only")}
|
|
1290
|
-
disabledReasonPlacement="inline"
|
|
1291
|
-
onPress={handleSave}
|
|
1292
|
-
>
|
|
1293
|
-
{isSubmitting ? t("Saving...") : t("Save")}
|
|
1294
|
-
</Button>
|
|
1295
|
-
)}
|
|
1296
|
-
</XStack>
|
|
1322
|
+
<FrappeFormActions
|
|
1323
|
+
indicator={indicator}
|
|
1324
|
+
action={docAction}
|
|
1325
|
+
busy={isSubmitting}
|
|
1326
|
+
disabled={effectiveDisabled}
|
|
1327
|
+
onSave={handleSave}
|
|
1328
|
+
onSubmit={handleSubmitDocument}
|
|
1329
|
+
onCancelDocument={handleCancelDocument}
|
|
1330
|
+
onAmend={showAmend ? handleAmend : undefined}
|
|
1331
|
+
amendedAs={amendedAs}
|
|
1332
|
+
onDelete={showDelete ? handleDelete : undefined}
|
|
1333
|
+
onEdit={showEdit ? onEdit : undefined}
|
|
1334
|
+
onClose={showClose ? handleCancel : undefined}
|
|
1335
|
+
closeDisabled={disabled}
|
|
1336
|
+
/>
|
|
1297
1337
|
)}
|
|
1298
1338
|
</YStack>
|
|
1299
1339
|
</XStack>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Chip, type ChipColor } from "@multiplatform.one/components";
|
|
2
|
+
import { Button } from "@multiplatform.one/forms";
|
|
3
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
4
|
+
import {
|
|
5
|
+
ArrowCounterClockwiseIcon,
|
|
6
|
+
CheckCircleIcon,
|
|
7
|
+
FloppyDiskIcon,
|
|
8
|
+
PencilSimpleIcon,
|
|
9
|
+
ProhibitIcon,
|
|
10
|
+
TrashIcon,
|
|
11
|
+
XIcon,
|
|
12
|
+
} from "@phosphor-icons/react";
|
|
13
|
+
import { useTranslation } from "react-i18next";
|
|
14
|
+
import { XStack } from "tamagui";
|
|
15
|
+
import type { DocAction, DocStatusIndicator } from "./formEngine/docLifecycle";
|
|
16
|
+
|
|
17
|
+
const indicatorColors: Record<DocStatusIndicator, ChipColor> = {
|
|
18
|
+
notSaved: "orange",
|
|
19
|
+
draft: "red",
|
|
20
|
+
submitted: "blue",
|
|
21
|
+
cancelled: "red",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface FrappeFormActionsProps {
|
|
25
|
+
/** Desk's state pill: Not Saved, Draft, Submitted or Cancelled. */
|
|
26
|
+
indicator?: DocStatusIndicator;
|
|
27
|
+
/** The one lifecycle action the document's state offers (resolveDocAction). */
|
|
28
|
+
action?: DocAction;
|
|
29
|
+
/** A save or a docstatus transition is in flight. */
|
|
30
|
+
busy: boolean;
|
|
31
|
+
/** The form is read-only: actions render, disabled with the reason. */
|
|
32
|
+
disabled: boolean;
|
|
33
|
+
onSave: () => void;
|
|
34
|
+
onSubmit: () => void;
|
|
35
|
+
/** Cancels the submitted document (docstatus 1 → 2). */
|
|
36
|
+
onCancelDocument: () => void;
|
|
37
|
+
/** Absent, a cancelled document offers no Amend: the host has nowhere to open the draft. */
|
|
38
|
+
onAmend?: () => void;
|
|
39
|
+
/** The live amendment of this cancelled document; Amend stays disabled while it exists. */
|
|
40
|
+
amendedAs?: string;
|
|
41
|
+
onDelete?: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Given, the form is a read-only view and this opens its editor. The
|
|
44
|
+
* caller leaves it out when the document has nothing left to edit.
|
|
45
|
+
*/
|
|
46
|
+
onEdit?: () => void;
|
|
47
|
+
/** The host's leave action (discards unsaved edits first). */
|
|
48
|
+
onClose?: () => void;
|
|
49
|
+
closeDisabled?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* FrappeForm's action bar with desk's docstatus lifecycle: one primary
|
|
54
|
+
* action at a time (Save, Submit, Update or Amend), Cancel Document as the
|
|
55
|
+
* secondary destructive action on a submitted document, and the state pill.
|
|
56
|
+
* A read-only view trades Save and the leave button for Edit.
|
|
57
|
+
*/
|
|
58
|
+
export function FrappeFormActions({
|
|
59
|
+
indicator,
|
|
60
|
+
action,
|
|
61
|
+
busy,
|
|
62
|
+
disabled,
|
|
63
|
+
onSave,
|
|
64
|
+
onSubmit,
|
|
65
|
+
onCancelDocument,
|
|
66
|
+
onAmend,
|
|
67
|
+
amendedAs,
|
|
68
|
+
onDelete,
|
|
69
|
+
onEdit,
|
|
70
|
+
onClose,
|
|
71
|
+
closeDisabled,
|
|
72
|
+
}: FrappeFormActionsProps) {
|
|
73
|
+
const { t } = useTranslation();
|
|
74
|
+
const { knobProps } = useResolvedKnobs();
|
|
75
|
+
const readOnlyReason = t("Form is read-only");
|
|
76
|
+
|
|
77
|
+
const indicatorLabels: Record<DocStatusIndicator, string> = {
|
|
78
|
+
notSaved: t("Not Saved"),
|
|
79
|
+
draft: t("Draft"),
|
|
80
|
+
submitted: t("Submitted"),
|
|
81
|
+
cancelled: t("Cancelled"),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<XStack
|
|
86
|
+
{...knobProps.gap}
|
|
87
|
+
justifyContent="flex-end"
|
|
88
|
+
alignItems="center"
|
|
89
|
+
paddingTop="$3"
|
|
90
|
+
testID="frappe-form-actions"
|
|
91
|
+
>
|
|
92
|
+
{indicator && (
|
|
93
|
+
<XStack flex={1} alignItems="center" testID={`frappe-form-status-${indicator}`}>
|
|
94
|
+
<Chip size="$2" variant="subtle" color={indicatorColors[indicator]}>
|
|
95
|
+
{indicatorLabels[indicator]}
|
|
96
|
+
</Chip>
|
|
97
|
+
</XStack>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{onDelete && (
|
|
101
|
+
<Button
|
|
102
|
+
outlined
|
|
103
|
+
error
|
|
104
|
+
icon={TrashIcon}
|
|
105
|
+
loading={busy}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
disabledReason={disabled ? readOnlyReason : undefined}
|
|
108
|
+
disabledReasonPlacement="inline"
|
|
109
|
+
onPress={onDelete}
|
|
110
|
+
>
|
|
111
|
+
{t("Delete")}
|
|
112
|
+
</Button>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{onClose && (
|
|
116
|
+
<Button
|
|
117
|
+
outlined
|
|
118
|
+
icon={XIcon}
|
|
119
|
+
disabled={closeDisabled}
|
|
120
|
+
disabledReason={closeDisabled ? readOnlyReason : undefined}
|
|
121
|
+
disabledReasonPlacement="inline"
|
|
122
|
+
onPress={onClose}
|
|
123
|
+
testID="frappe-form-close"
|
|
124
|
+
>
|
|
125
|
+
{/* A submitted document's own Cancel sits beside this button. */}
|
|
126
|
+
{action === "cancel" ? t("Close") : t("Cancel")}
|
|
127
|
+
</Button>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{action === "cancel" && (
|
|
131
|
+
<Button
|
|
132
|
+
outlined
|
|
133
|
+
error
|
|
134
|
+
actionRole="secondary"
|
|
135
|
+
icon={ProhibitIcon}
|
|
136
|
+
loading={busy}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
disabledReason={disabled ? readOnlyReason : undefined}
|
|
139
|
+
disabledReasonPlacement="inline"
|
|
140
|
+
onPress={onCancelDocument}
|
|
141
|
+
testID="frappe-form-cancel-document"
|
|
142
|
+
>
|
|
143
|
+
{t("Cancel Document")}
|
|
144
|
+
</Button>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{onEdit && (
|
|
148
|
+
<Button
|
|
149
|
+
accent={action !== "submit"}
|
|
150
|
+
outlined={action === "submit"}
|
|
151
|
+
icon={PencilSimpleIcon}
|
|
152
|
+
disabled={disabled}
|
|
153
|
+
disabledReason={disabled ? readOnlyReason : undefined}
|
|
154
|
+
disabledReasonPlacement="inline"
|
|
155
|
+
onPress={onEdit}
|
|
156
|
+
testID="frappe-form-edit"
|
|
157
|
+
>
|
|
158
|
+
{t("Edit")}
|
|
159
|
+
</Button>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{action === "amend" && onAmend && (
|
|
163
|
+
<Button
|
|
164
|
+
// Desk demotes Amend to a secondary action once the document is amended.
|
|
165
|
+
accent={!amendedAs}
|
|
166
|
+
outlined={Boolean(amendedAs)}
|
|
167
|
+
icon={ArrowCounterClockwiseIcon}
|
|
168
|
+
loading={busy}
|
|
169
|
+
disabled={disabled || Boolean(amendedAs)}
|
|
170
|
+
disabledReason={
|
|
171
|
+
amendedAs
|
|
172
|
+
? t("Already amended as {{name}}", { name: amendedAs })
|
|
173
|
+
: disabled
|
|
174
|
+
? readOnlyReason
|
|
175
|
+
: undefined
|
|
176
|
+
}
|
|
177
|
+
disabledReasonPlacement="inline"
|
|
178
|
+
onPress={onAmend}
|
|
179
|
+
testID="frappe-form-amend"
|
|
180
|
+
>
|
|
181
|
+
{t("Amend")}
|
|
182
|
+
</Button>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{action === "submit" && (
|
|
186
|
+
<Button
|
|
187
|
+
accent
|
|
188
|
+
icon={CheckCircleIcon}
|
|
189
|
+
loading={busy}
|
|
190
|
+
disabled={disabled}
|
|
191
|
+
disabledReason={disabled ? readOnlyReason : undefined}
|
|
192
|
+
disabledReasonPlacement="inline"
|
|
193
|
+
onPress={onSubmit}
|
|
194
|
+
testID="frappe-form-submit"
|
|
195
|
+
>
|
|
196
|
+
{t("Submit")}
|
|
197
|
+
</Button>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{(action === "save" || action === "update") && (
|
|
201
|
+
<Button
|
|
202
|
+
accent
|
|
203
|
+
icon={FloppyDiskIcon}
|
|
204
|
+
loading={busy}
|
|
205
|
+
disabled={disabled}
|
|
206
|
+
disabledReason={disabled ? readOnlyReason : undefined}
|
|
207
|
+
disabledReasonPlacement="inline"
|
|
208
|
+
onPress={onSave}
|
|
209
|
+
testID="frappe-form-save"
|
|
210
|
+
>
|
|
211
|
+
{busy ? t("Saving...") : action === "update" ? t("Update") : t("Save")}
|
|
212
|
+
</Button>
|
|
213
|
+
)}
|
|
214
|
+
</XStack>
|
|
215
|
+
);
|
|
216
|
+
}
|