@multiplatform.one/frappe-ui 7.22.0 → 7.23.1
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/cjs/useFrappeAggregate.cjs +1 -1
- package/dist/cjs/useFrappeAggregate.native.js +1 -1
- package/dist/cjs/useFrappeAggregate.native.js.map +1 -1
- 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/esm/useFrappeAggregate.mjs +1 -1
- package/dist/esm/useFrappeAggregate.mjs.map +1 -1
- package/dist/esm/useFrappeAggregate.native.js +1 -1
- package/dist/esm/useFrappeAggregate.native.js.map +1 -1
- 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/dist/jsx/useFrappeAggregate.mjs +1 -1
- package/dist/jsx/useFrappeAggregate.mjs.map +1 -1
- package/dist/jsx/useFrappeAggregate.native.js +1 -1
- package/dist/jsx/useFrappeAggregate.native.js.map +1 -1
- 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/src/useFrappeAggregate.ts +1 -1
- 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/filterFields.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The one filterable-field vocabulary (MPO-15, LC-16).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Which doctype fields can filter, the standard doc fields every doctype
|
|
5
|
+
* carries, how they are labelled, desk's operator vocabulary (=, !=, like,
|
|
6
|
+
* not like, in, not in, comparisons, between, is set / is not set) and the
|
|
7
|
+
* two-way frappe tuple conversion. Both list stacks read it: the desk list
|
|
8
|
+
* chrome (filter editor, chips, quick filters) takes the descriptors, and
|
|
9
|
+
* FrappeTable's toolbar takes `docFieldsToFilterFields`, the same fields on
|
|
10
|
+
* the table's `filterableFields` contract. Table filter types come from the
|
|
11
|
+
* field registry (fieldtypeToTableField) and the table's own field→filter
|
|
12
|
+
* type mapping: one chain, no parallel fieldtype table.
|
|
10
13
|
*/
|
|
11
|
-
|
|
12
14
|
import type { DocTypeField } from "@multiplatform.one/frappe";
|
|
13
15
|
import {
|
|
14
16
|
type DataTableField,
|
|
@@ -16,6 +18,21 @@ import {
|
|
|
16
18
|
mapFieldToFilterType,
|
|
17
19
|
} from "@multiplatform.one/table";
|
|
18
20
|
import { fieldtypeToTableField } from "./fieldRegistry";
|
|
21
|
+
import type { DocFieldMeta, DoctypeMeta } from "./formEngine/doctypeMeta";
|
|
22
|
+
import type { FrappeFieldFilter } from "./useConnectionFilters";
|
|
23
|
+
|
|
24
|
+
// ── Field descriptors ─────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export type FilterFieldKind = "select" | "link" | "check" | "date" | "number" | "text";
|
|
27
|
+
|
|
28
|
+
export interface FilterFieldDescriptor {
|
|
29
|
+
fieldname: string;
|
|
30
|
+
label: string;
|
|
31
|
+
fieldtype: string;
|
|
32
|
+
kind: FilterFieldKind;
|
|
33
|
+
/** Link target doctype, or the raw Select options string. */
|
|
34
|
+
options?: string;
|
|
35
|
+
}
|
|
19
36
|
|
|
20
37
|
/** Layout / non-data fieldtypes that cannot meaningfully filter. */
|
|
21
38
|
const NON_FILTERABLE_FIELDTYPES = new Set([
|
|
@@ -34,44 +51,354 @@ const NON_FILTERABLE_FIELDTYPES = new Set([
|
|
|
34
51
|
"Password",
|
|
35
52
|
]);
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
{ id: "name", label: "ID", type: "text" },
|
|
40
|
-
{ id: "owner", label: "Created By", type: "text" },
|
|
41
|
-
{ id: "creation", label: "Created On", type: "date" },
|
|
42
|
-
{ id: "modified", label: "Last Updated On", type: "date" },
|
|
43
|
-
];
|
|
54
|
+
const NUMBER_FIELDTYPES = new Set(["Int", "Float", "Currency", "Percent", "Duration", "Rating"]);
|
|
55
|
+
const DATE_FIELDTYPES = new Set(["Date", "Datetime", "Time"]);
|
|
44
56
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
/** Editor kind for a frappe fieldtype (Dynamic Link has no static target → text). */
|
|
58
|
+
export function fieldKind(fieldtype: string): FilterFieldKind {
|
|
59
|
+
if (fieldtype === "Select") return "select";
|
|
60
|
+
if (fieldtype === "Link") return "link";
|
|
61
|
+
if (fieldtype === "Check") return "check";
|
|
62
|
+
if (DATE_FIELDTYPES.has(fieldtype)) return "date";
|
|
63
|
+
if (NUMBER_FIELDTYPES.has(fieldtype)) return "number";
|
|
64
|
+
return "text";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toDescriptor(field: DocFieldMeta): FilterFieldDescriptor {
|
|
68
|
+
return {
|
|
69
|
+
fieldname: field.fieldname,
|
|
70
|
+
label: field.label || field.fieldname,
|
|
71
|
+
fieldtype: field.fieldtype,
|
|
72
|
+
kind: fieldKind(field.fieldtype),
|
|
73
|
+
options: field.options,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Standard doc fields present on every doctype but absent from meta.fields. */
|
|
78
|
+
export function standardDocFilterFields(): FilterFieldDescriptor[] {
|
|
79
|
+
return [
|
|
80
|
+
{ fieldname: "name", label: "ID", fieldtype: "Data", kind: "text" },
|
|
81
|
+
{ fieldname: "owner", label: "Created By", fieldtype: "Link", kind: "link", options: "User" },
|
|
82
|
+
{ fieldname: "creation", label: "Created On", fieldtype: "Datetime", kind: "date" },
|
|
83
|
+
{ fieldname: "modified", label: "Last Updated On", fieldtype: "Datetime", kind: "date" },
|
|
84
|
+
{
|
|
85
|
+
fieldname: "modified_by",
|
|
86
|
+
label: "Last Updated By",
|
|
87
|
+
fieldtype: "Link",
|
|
88
|
+
kind: "link",
|
|
89
|
+
options: "User",
|
|
90
|
+
},
|
|
91
|
+
// Desk `_` facet columns — the sidebar facets and heart column write
|
|
92
|
+
// tuples on these, so chips and the editor must label them (desk offers
|
|
93
|
+
// them in its filter field picker too). Never surface the raw
|
|
94
|
+
// underscore fieldname (no-raw-fieldname rule candidate).
|
|
95
|
+
{ fieldname: "_assign", label: "Assigned To", fieldtype: "Data", kind: "text" },
|
|
96
|
+
{ fieldname: "_user_tags", label: "Tags", fieldtype: "Data", kind: "text" },
|
|
97
|
+
{ fieldname: "_liked_by", label: "Liked By", fieldtype: "Data", kind: "text" },
|
|
98
|
+
{ fieldname: "_comments", label: "Comments", fieldtype: "Data", kind: "text" },
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function filterableDescriptors(fields: readonly DocFieldMeta[]): FilterFieldDescriptor[] {
|
|
103
|
+
const out: FilterFieldDescriptor[] = [];
|
|
52
104
|
const seen = new Set<string>();
|
|
53
105
|
for (const field of fields) {
|
|
54
106
|
if (!field.fieldname || seen.has(field.fieldname)) continue;
|
|
55
107
|
const fieldtype = String(field.fieldtype);
|
|
56
108
|
if (NON_FILTERABLE_FIELDTYPES.has(fieldtype) || fieldtype.startsWith("Table")) continue;
|
|
57
|
-
const tableField = fieldtypeToTableField(fieldtype, field.options);
|
|
58
|
-
const options = (tableField.fieldProps as { options?: Array<{ label: string; value: any }> })
|
|
59
|
-
?.options;
|
|
60
109
|
seen.add(field.fieldname);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
110
|
+
out.push(toDescriptor(field));
|
|
111
|
+
}
|
|
112
|
+
for (const standard of standardDocFilterFields()) {
|
|
113
|
+
if (!seen.has(standard.fieldname)) out.push(standard);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* All fields offered by the filter editor's field picker — data fields from
|
|
120
|
+
* meta (hidden ones included, desk parity) plus the standard doc fields.
|
|
121
|
+
*/
|
|
122
|
+
export function filterableFieldsFromMeta(meta?: DoctypeMeta): FilterFieldDescriptor[] {
|
|
123
|
+
return filterableDescriptors(meta?.fields ?? []);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toTableFilterField(field: FilterFieldDescriptor): FilterableField {
|
|
127
|
+
const tableField = fieldtypeToTableField(field.fieldtype, field.options);
|
|
128
|
+
const options = (tableField.fieldProps as { options?: Array<{ label: string; value: any }> })
|
|
129
|
+
?.options;
|
|
130
|
+
return {
|
|
131
|
+
id: field.fieldname,
|
|
132
|
+
label: field.label,
|
|
133
|
+
// Rating cells map to a custom render (fraction contract) which would
|
|
134
|
+
// otherwise fall through to "text"; it filters as the number it is.
|
|
135
|
+
type:
|
|
136
|
+
field.fieldtype === "Rating"
|
|
137
|
+
? "number"
|
|
138
|
+
: mapFieldToFilterType(tableField.field as DataTableField),
|
|
139
|
+
...(options ? { options } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The same vocabulary on the table's `filterableFields` contract: the
|
|
145
|
+
* doctype's filterable fields plus the standard doc fields, typed through
|
|
146
|
+
* the field registry. FrappeTable derives its toolbar menu from this.
|
|
147
|
+
*/
|
|
148
|
+
export function docFieldsToFilterFields(fields: DocTypeField[]): FilterableField[] {
|
|
149
|
+
return filterableDescriptors(fields).map(toTableFilterField);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Fields flagged `in_standard_filter`, in meta order (the quick-filter row).
|
|
153
|
+
* Desk always offers ID plus the title field as live header inputs even when
|
|
154
|
+
* those flags are off — typing a fragment of ID/subject is the fastest
|
|
155
|
+
* narrowing habit list users have (audit E1). */
|
|
156
|
+
export function standardFiltersFromMeta(meta?: DoctypeMeta): FilterFieldDescriptor[] {
|
|
157
|
+
const fromMeta = (meta?.fields ?? [])
|
|
158
|
+
.filter((field) => field.in_standard_filter === 1 && field.hidden !== 1 && field.fieldname)
|
|
159
|
+
.map(toDescriptor);
|
|
160
|
+
const seen = new Set(fromMeta.map((field) => field.fieldname));
|
|
161
|
+
const extras: FilterFieldDescriptor[] = [];
|
|
162
|
+
if (!seen.has("name")) {
|
|
163
|
+
extras.push({ fieldname: "name", label: "ID", fieldtype: "Data", kind: "text" });
|
|
164
|
+
seen.add("name");
|
|
165
|
+
}
|
|
166
|
+
const titleField = meta?.title_field;
|
|
167
|
+
if (titleField && !seen.has(titleField)) {
|
|
168
|
+
const titleMeta = meta?.fields?.find((field) => field.fieldname === titleField);
|
|
169
|
+
extras.push({
|
|
170
|
+
fieldname: titleField,
|
|
171
|
+
label: titleMeta?.label || "Title",
|
|
172
|
+
fieldtype: titleMeta?.fieldtype || "Data",
|
|
173
|
+
kind: fieldKind(titleMeta?.fieldtype || "Data"),
|
|
71
174
|
});
|
|
72
175
|
}
|
|
73
|
-
|
|
74
|
-
|
|
176
|
+
return extras.length > 0 ? [...extras, ...fromMeta] : fromMeta;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Select fieldtype options string → dropdown options (blank lines dropped). */
|
|
180
|
+
export function selectOptionsFromField(options?: string): Array<{ label: string; value: string }> {
|
|
181
|
+
return (options ?? "")
|
|
182
|
+
.split("\n")
|
|
183
|
+
.map((line) => line.trim())
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.map((line) => ({ label: line, value: line }));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Human label for a fieldname — descriptor label when known, else a
|
|
190
|
+
* title-cased form. The raw identifier (underscores) never reaches chips or
|
|
191
|
+
* editor rows (no-raw-fieldname rule candidate, parity audit pair 04/16).
|
|
192
|
+
*/
|
|
193
|
+
export function fieldLabelFor(fields: FilterFieldDescriptor[], fieldname: string): string {
|
|
194
|
+
const known = fields.find((field) => field.fieldname === fieldname)?.label;
|
|
195
|
+
if (known) return known;
|
|
196
|
+
const pretty = fieldname
|
|
197
|
+
.replace(/^_+/, "")
|
|
198
|
+
.split("_")
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
201
|
+
.join(" ");
|
|
202
|
+
return pretty || fieldname;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Desk operator vocabulary ──────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export type DeskOperator =
|
|
208
|
+
| "="
|
|
209
|
+
| "!="
|
|
210
|
+
| "like"
|
|
211
|
+
| "not like"
|
|
212
|
+
| "in"
|
|
213
|
+
| "not in"
|
|
214
|
+
| ">"
|
|
215
|
+
| "<"
|
|
216
|
+
| ">="
|
|
217
|
+
| "<="
|
|
218
|
+
| "between"
|
|
219
|
+
| "is set"
|
|
220
|
+
| "is not set";
|
|
221
|
+
|
|
222
|
+
/** English operator labels — translate at render time via t(). */
|
|
223
|
+
export const DESK_OPERATOR_LABELS: Record<DeskOperator, string> = {
|
|
224
|
+
"=": "Equals",
|
|
225
|
+
"!=": "Not Equals",
|
|
226
|
+
like: "Like",
|
|
227
|
+
"not like": "Not Like",
|
|
228
|
+
in: "In",
|
|
229
|
+
"not in": "Not In",
|
|
230
|
+
">": "Greater Than",
|
|
231
|
+
"<": "Less Than",
|
|
232
|
+
">=": "Greater Than or Equal",
|
|
233
|
+
"<=": "Less Than or Equal",
|
|
234
|
+
between: "Between",
|
|
235
|
+
"is set": "Is Set",
|
|
236
|
+
"is not set": "Is Not Set",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const BASE_OPERATORS: DeskOperator[] = [
|
|
240
|
+
"=",
|
|
241
|
+
"!=",
|
|
242
|
+
"like",
|
|
243
|
+
"not like",
|
|
244
|
+
"in",
|
|
245
|
+
"not in",
|
|
246
|
+
"is set",
|
|
247
|
+
"is not set",
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
/** Operators offered for an editor kind (desk's list, comparisons gated). */
|
|
251
|
+
export function operatorsForKind(kind: FilterFieldKind): DeskOperator[] {
|
|
252
|
+
switch (kind) {
|
|
253
|
+
case "check":
|
|
254
|
+
return ["=", "!="];
|
|
255
|
+
case "number":
|
|
256
|
+
case "date":
|
|
257
|
+
return ["=", "!=", ">", "<", ">=", "<=", "between", "in", "not in", "is set", "is not set"];
|
|
258
|
+
default:
|
|
259
|
+
return BASE_OPERATORS;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Initial operator when a field is picked (desk: text searches, others match). */
|
|
264
|
+
export function defaultOperatorForKind(kind: FilterFieldKind): DeskOperator {
|
|
265
|
+
return kind === "text" ? "like" : "=";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Editor row model + tuple conversion ───────────────────────
|
|
269
|
+
|
|
270
|
+
export interface FilterRowDraft {
|
|
271
|
+
fieldname: string;
|
|
272
|
+
operator: DeskOperator;
|
|
273
|
+
/** Scalar editor value (single input / select / link / comma list). */
|
|
274
|
+
value: string;
|
|
275
|
+
/** Multi values: in / not in multi-select, or [from, to] for between. */
|
|
276
|
+
values: string[];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Draft → frappe tuple; null while the draft is incomplete.
|
|
281
|
+
* `like` wraps `%value%` unless the user typed their own wildcard;
|
|
282
|
+
* `is set` / `is not set` become ["field", "is", "set" | "not set"].
|
|
283
|
+
*/
|
|
284
|
+
export function draftToTuple(
|
|
285
|
+
draft: FilterRowDraft,
|
|
286
|
+
kind: FilterFieldKind,
|
|
287
|
+
): FrappeFieldFilter | null {
|
|
288
|
+
const { fieldname, operator } = draft;
|
|
289
|
+
if (!fieldname) return null;
|
|
290
|
+
switch (operator) {
|
|
291
|
+
case "is set":
|
|
292
|
+
return [fieldname, "is", "set"];
|
|
293
|
+
case "is not set":
|
|
294
|
+
return [fieldname, "is", "not set"];
|
|
295
|
+
case "in":
|
|
296
|
+
case "not in": {
|
|
297
|
+
const values =
|
|
298
|
+
kind === "select"
|
|
299
|
+
? draft.values.map((value) => value.trim()).filter(Boolean)
|
|
300
|
+
: draft.value
|
|
301
|
+
.split(",")
|
|
302
|
+
.map((value) => value.trim())
|
|
303
|
+
.filter(Boolean);
|
|
304
|
+
return values.length ? [fieldname, operator, values] : null;
|
|
305
|
+
}
|
|
306
|
+
case "between": {
|
|
307
|
+
const from = (draft.values[0] ?? "").trim();
|
|
308
|
+
const to = (draft.values[1] ?? "").trim();
|
|
309
|
+
return from && to ? [fieldname, "between", [from, to]] : null;
|
|
310
|
+
}
|
|
311
|
+
case "like":
|
|
312
|
+
case "not like": {
|
|
313
|
+
const text = draft.value.trim();
|
|
314
|
+
if (!text) return null;
|
|
315
|
+
return [fieldname, operator, text.includes("%") ? text : `%${text}%`];
|
|
316
|
+
}
|
|
317
|
+
default: {
|
|
318
|
+
const text = draft.value.trim();
|
|
319
|
+
return text ? [fieldname, operator, text] : null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const SCALAR_OPERATORS: readonly string[] = ["=", "!=", "like", "not like", ">", "<", ">=", "<="];
|
|
325
|
+
|
|
326
|
+
/** Frappe tuple → editable draft (inverse of draftToTuple). */
|
|
327
|
+
export function tupleToDraft(tuple: FrappeFieldFilter): FilterRowDraft {
|
|
328
|
+
const [fieldname, operator, raw] = tuple;
|
|
329
|
+
if (operator === "is") {
|
|
330
|
+
return {
|
|
331
|
+
fieldname,
|
|
332
|
+
operator: raw === "set" ? "is set" : "is not set",
|
|
333
|
+
value: "",
|
|
334
|
+
values: [],
|
|
335
|
+
};
|
|
75
336
|
}
|
|
76
|
-
|
|
337
|
+
if (operator === "in" || operator === "not in") {
|
|
338
|
+
const values = Array.isArray(raw)
|
|
339
|
+
? raw.map(String)
|
|
340
|
+
: String(raw ?? "")
|
|
341
|
+
.split(",")
|
|
342
|
+
.map((value) => value.trim())
|
|
343
|
+
.filter(Boolean);
|
|
344
|
+
return { fieldname, operator, value: values.join(", "), values };
|
|
345
|
+
}
|
|
346
|
+
if (operator === "between") {
|
|
347
|
+
const values = Array.isArray(raw) ? raw.map(String) : [];
|
|
348
|
+
return {
|
|
349
|
+
fieldname,
|
|
350
|
+
operator: "between",
|
|
351
|
+
value: "",
|
|
352
|
+
values: [values[0] ?? "", values[1] ?? ""],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const known = SCALAR_OPERATORS.includes(operator) ? (operator as DeskOperator) : "=";
|
|
356
|
+
return {
|
|
357
|
+
fieldname,
|
|
358
|
+
operator: known,
|
|
359
|
+
value: raw === null || raw === undefined ? "" : String(raw),
|
|
360
|
+
values: [],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Filter list helpers ───────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Quick-filter semantics: a standard filter control owns every tuple for its
|
|
368
|
+
* fieldname — replace them with `tuple`, or clear when `tuple` is undefined.
|
|
369
|
+
*/
|
|
370
|
+
export function setFieldFilter(
|
|
371
|
+
filters: FrappeFieldFilter[],
|
|
372
|
+
fieldname: string,
|
|
373
|
+
tuple?: FrappeFieldFilter,
|
|
374
|
+
): FrappeFieldFilter[] {
|
|
375
|
+
const rest = filters.filter(([name]) => name !== fieldname);
|
|
376
|
+
return tuple ? [...rest, tuple] : rest;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The active quick-filter display value for a fieldname ("" when none). */
|
|
380
|
+
export function quickFilterValue(filters: FrappeFieldFilter[], fieldname: string): string {
|
|
381
|
+
const tuple = filters.find(([name]) => name === fieldname);
|
|
382
|
+
if (!tuple) return "";
|
|
383
|
+
const [, operator, raw] = tuple;
|
|
384
|
+
if (operator === "like") {
|
|
385
|
+
const text = String(raw ?? "");
|
|
386
|
+
return text.startsWith("%") && text.endsWith("%") && text.length >= 2
|
|
387
|
+
? text.slice(1, -1)
|
|
388
|
+
: text;
|
|
389
|
+
}
|
|
390
|
+
if (operator === "=") return raw === null || raw === undefined ? "" : String(raw);
|
|
391
|
+
return "";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Chip label: "Status = Open", "Due Date between a, b", "Owner is set". */
|
|
395
|
+
export function filterTupleLabel(
|
|
396
|
+
fields: FilterFieldDescriptor[],
|
|
397
|
+
tuple: FrappeFieldFilter,
|
|
398
|
+
): string {
|
|
399
|
+
const [fieldname, operator, raw] = tuple;
|
|
400
|
+
const label = fieldLabelFor(fields, fieldname);
|
|
401
|
+
if (operator === "is") return `${label} ${raw === "set" ? "is set" : "is not set"}`;
|
|
402
|
+
const value = Array.isArray(raw) ? raw.map(String).join(", ") : String(raw ?? "");
|
|
403
|
+
return `${label} ${operator} ${value}`;
|
|
77
404
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildAmendValues,
|
|
4
|
+
canDeleteDoc,
|
|
5
|
+
canEditDoc,
|
|
6
|
+
isAmendable,
|
|
7
|
+
resolveDocAction,
|
|
8
|
+
resolveDocIndicator,
|
|
9
|
+
type DocActionPerms,
|
|
10
|
+
} from "./docLifecycle";
|
|
11
|
+
import { resolveDocRights } from "./docPermissions";
|
|
12
|
+
import type { DocFieldMeta, DocPermissionRule } from "./doctypeMeta";
|
|
13
|
+
|
|
14
|
+
const all: DocActionPerms = { write: true, submit: true, cancel: true, amend: true };
|
|
15
|
+
const none: DocActionPerms = { write: false, submit: false, cancel: false, amend: false };
|
|
16
|
+
|
|
17
|
+
function action(
|
|
18
|
+
docstatus: number,
|
|
19
|
+
overrides: {
|
|
20
|
+
isCreate?: boolean;
|
|
21
|
+
isDirty?: boolean;
|
|
22
|
+
isSubmittable?: boolean;
|
|
23
|
+
isReadOnlyView?: boolean;
|
|
24
|
+
} = {},
|
|
25
|
+
perms: DocActionPerms = all,
|
|
26
|
+
) {
|
|
27
|
+
return resolveDocAction({
|
|
28
|
+
docstatus,
|
|
29
|
+
isCreate: false,
|
|
30
|
+
isDirty: false,
|
|
31
|
+
isSubmittable: true,
|
|
32
|
+
perms,
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("resolveDocAction follows desk get_action_status", () => {
|
|
38
|
+
it("a saved, clean draft submits", () => {
|
|
39
|
+
expect(action(0)).toBe("submit");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("a new or dirty draft saves", () => {
|
|
43
|
+
expect(action(0, { isCreate: true })).toBe("save");
|
|
44
|
+
expect(action(0, { isDirty: true })).toBe("save");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("a draft saves when the roles cannot submit", () => {
|
|
48
|
+
expect(action(0, {}, { ...all, submit: false })).toBe("save");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Save and Update need write, which is create on a new document", () => {
|
|
52
|
+
const readOnly = { ...all, write: false };
|
|
53
|
+
expect(action(0, { isCreate: true }, readOnly)).toBeUndefined();
|
|
54
|
+
expect(action(0, { isDirty: true }, readOnly)).toBeUndefined();
|
|
55
|
+
expect(action(0, { isSubmittable: false }, readOnly)).toBeUndefined();
|
|
56
|
+
expect(action(0, {}, readOnly)).toBe("submit");
|
|
57
|
+
expect(action(1, { isDirty: true }, readOnly)).toBe("cancel");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("a doctype that is not submittable only ever saves", () => {
|
|
61
|
+
expect(action(0, { isSubmittable: false })).toBe("save");
|
|
62
|
+
expect(action(1, { isSubmittable: false })).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("a submitted document cancels, or updates while an allow_on_submit edit is pending", () => {
|
|
66
|
+
expect(action(1)).toBe("cancel");
|
|
67
|
+
expect(action(1, { isDirty: true })).toBe("update");
|
|
68
|
+
expect(action(1, { isDirty: true }, { ...all, submit: false })).toBe("cancel");
|
|
69
|
+
expect(action(1, {}, { ...all, cancel: false })).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("a cancelled document amends, given the right", () => {
|
|
73
|
+
expect(action(2)).toBe("amend");
|
|
74
|
+
expect(action(2, {}, none)).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("a read-only view drops Save and Update for its Edit and keeps Submit, Cancel and Amend", () => {
|
|
78
|
+
const view = { isReadOnlyView: true };
|
|
79
|
+
expect(action(0, view)).toBe("submit");
|
|
80
|
+
expect(action(0, { ...view, isSubmittable: false })).toBeUndefined();
|
|
81
|
+
expect(action(0, view, { ...all, submit: false })).toBeUndefined();
|
|
82
|
+
expect(action(1, view)).toBe("cancel");
|
|
83
|
+
expect(action(1, { ...view, isDirty: true })).toBeUndefined();
|
|
84
|
+
expect(action(2, view)).toBe("amend");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("canEditDoc", () => {
|
|
89
|
+
const title: DocFieldMeta = { fieldname: "title", fieldtype: "Data" };
|
|
90
|
+
const remarks: DocFieldMeta = { fieldname: "remarks", fieldtype: "Data", allow_on_submit: 1 };
|
|
91
|
+
const plain = { fields: [title] };
|
|
92
|
+
const onSubmit = { fields: [title, remarks] };
|
|
93
|
+
|
|
94
|
+
it("edits any draft, a submitted document only through allow_on_submit, a cancelled one never", () => {
|
|
95
|
+
expect(canEditDoc(0, plain)).toBe(true);
|
|
96
|
+
expect(canEditDoc(0, undefined)).toBe(true);
|
|
97
|
+
expect(canEditDoc(1, plain)).toBe(false);
|
|
98
|
+
expect(canEditDoc(1, onSubmit)).toBe(true);
|
|
99
|
+
expect(canEditDoc(2, onSubmit)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("the toolbar reads the session's level-0 rights", () => {
|
|
104
|
+
const rules: DocPermissionRule[] = [
|
|
105
|
+
{ role: "Clerk", permlevel: 0, read: 1, write: 1, submit: 1 },
|
|
106
|
+
{ role: "Manager", permlevel: 0, read: 1, cancel: 1, amend: 1 },
|
|
107
|
+
{ role: "Auditor", permlevel: 1, read: 1, write: 1, submit: 1, cancel: 1, amend: 1 },
|
|
108
|
+
];
|
|
109
|
+
const perms = (roles: string[]) => resolveDocRights({ permissions: rules, roles });
|
|
110
|
+
|
|
111
|
+
it("a Clerk saves and submits but neither cancels nor amends", () => {
|
|
112
|
+
expect(action(0, {}, perms(["Clerk"]))).toBe("submit");
|
|
113
|
+
expect(action(1, {}, perms(["Clerk"]))).toBeUndefined();
|
|
114
|
+
expect(action(2, {}, perms(["Clerk"]))).toBeUndefined();
|
|
115
|
+
expect(action(2, {}, perms(["Clerk", "Manager"]))).toBe("amend");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("rows above level 0 grant no lifecycle action", () => {
|
|
119
|
+
expect(action(0, { isDirty: true }, perms(["Auditor"]))).toBeUndefined();
|
|
120
|
+
expect(action(1, {}, perms(["Auditor"]))).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("desk indicator and delete", () => {
|
|
125
|
+
it("names the state the way the desk title pill does", () => {
|
|
126
|
+
const base = { docstatus: 0, isCreate: false, isDirty: false, isSubmittable: true };
|
|
127
|
+
expect(resolveDocIndicator({ ...base, isCreate: true })).toBe("notSaved");
|
|
128
|
+
expect(resolveDocIndicator({ ...base, isDirty: true, docstatus: 1 })).toBe("notSaved");
|
|
129
|
+
expect(resolveDocIndicator(base)).toBe("draft");
|
|
130
|
+
expect(resolveDocIndicator({ ...base, docstatus: 1 })).toBe("submitted");
|
|
131
|
+
expect(resolveDocIndicator({ ...base, docstatus: 2 })).toBe("cancelled");
|
|
132
|
+
expect(resolveDocIndicator({ ...base, isSubmittable: false })).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("never deletes a submitted document", () => {
|
|
136
|
+
expect(canDeleteDoc(0)).toBe(true);
|
|
137
|
+
expect(canDeleteDoc(1)).toBe(false);
|
|
138
|
+
expect(canDeleteDoc(2)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("buildAmendValues follows desk copy_doc(from_amend)", () => {
|
|
143
|
+
const fields: DocFieldMeta[] = [
|
|
144
|
+
{ fieldname: "title", fieldtype: "Data" },
|
|
145
|
+
{ fieldname: "ref_no", fieldtype: "Data", no_copy: 1 },
|
|
146
|
+
{ fieldname: "secret", fieldtype: "Password" },
|
|
147
|
+
{ fieldname: "cancel_reason", fieldtype: "Small Text" },
|
|
148
|
+
{ fieldname: "items", fieldtype: "Table", options: "Loan Item" },
|
|
149
|
+
{ fieldname: "amendment_date", fieldtype: "Date" },
|
|
150
|
+
{ fieldname: "amended_from", fieldtype: "Link", options: "Asset Loan", no_copy: 1 },
|
|
151
|
+
];
|
|
152
|
+
const cancelled = {
|
|
153
|
+
name: "LOAN-1",
|
|
154
|
+
docstatus: 2,
|
|
155
|
+
title: "Laptop",
|
|
156
|
+
ref_no: "R-7",
|
|
157
|
+
secret: "hunter2",
|
|
158
|
+
cancel_reason: "Wrong serial",
|
|
159
|
+
amended_from: "LOAN-0",
|
|
160
|
+
items: [
|
|
161
|
+
{
|
|
162
|
+
name: "row-1",
|
|
163
|
+
parent: "LOAN-1",
|
|
164
|
+
parentfield: "items",
|
|
165
|
+
parenttype: "Asset Loan",
|
|
166
|
+
docstatus: 2,
|
|
167
|
+
idx: 1,
|
|
168
|
+
item: "Charger",
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
it("copies no_copy fields, skips passwords and the cancel reason, points at the cancelled doc", () => {
|
|
174
|
+
const draft = buildAmendValues(fields, cancelled, "LOAN-1");
|
|
175
|
+
expect(draft.title).toBe("Laptop");
|
|
176
|
+
expect(draft.ref_no).toBe("R-7");
|
|
177
|
+
expect(draft).not.toHaveProperty("secret");
|
|
178
|
+
expect(draft).not.toHaveProperty("cancel_reason");
|
|
179
|
+
expect(draft).not.toHaveProperty("name");
|
|
180
|
+
expect(draft.amended_from).toBe("LOAN-1");
|
|
181
|
+
expect(draft.docstatus).toBe(0);
|
|
182
|
+
expect(draft.amendment_date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("strips child rows of their identity", () => {
|
|
186
|
+
const draft = buildAmendValues(fields, cancelled, "LOAN-1");
|
|
187
|
+
expect(draft.items).toEqual([{ idx: 1, item: "Charger" }]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("amends only a doctype that carries amended_from", () => {
|
|
191
|
+
expect(isAmendable({ fields })).toBe(true);
|
|
192
|
+
expect(isAmendable({ fields: fields.slice(0, 2) })).toBe(false);
|
|
193
|
+
expect(isAmendable(undefined)).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|