@isi-ui7/bos7-shared 0.2.4 → 0.2.7
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/dist/crud-components.d.ts +2 -0
- package/dist/crud-hooks.d.ts +1 -1
- package/dist/form-numeric.d.ts +12 -0
- package/dist/form-renderer.d.ts +4 -2
- package/dist/form-types.d.ts +75 -3
- package/dist/form-value-schema.d.ts +118 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4768 -4534
- package/dist/shell/app-shell-layout.d.ts +21 -6
- package/dist/style-contract.d.ts +20 -5
- package/dist/workflow/starter.d.ts +13 -0
- package/package.json +10 -7
- package/src/crud-components.tsx +27 -2
- package/src/crud-hooks.ts +5 -2
- package/src/form-contract.css +64 -2
- package/src/form-numeric.ts +16 -0
- package/src/form-renderer.tsx +381 -18
- package/src/form-types.ts +83 -5
- package/src/form-value-schema.test.ts +263 -0
- package/src/form-value-schema.ts +500 -0
- package/src/index.ts +1 -0
- package/src/shell/app-shell-layout.tsx +22 -46
- package/src/style-contract.test.ts +1 -0
- package/src/style-contract.ts +38 -5
- package/src/workflow/starter.ts +48 -11
package/src/form-types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { Ui7FormDensity } from "./style-contract";
|
|
2
|
+
import type { Ui7FormDensity, Ui7FormWidth } from "./style-contract";
|
|
3
3
|
import type { T_LookupTblStruct } from "@isi-ui7/lookup-input";
|
|
4
|
+
import type { EditableColumnDef } from "@isi-ui7/editable-table";
|
|
4
5
|
|
|
5
6
|
export type FormMode = "create" | "edit" | "view";
|
|
6
7
|
|
|
@@ -30,10 +31,17 @@ export type FieldValidation<TData extends Record<string, unknown>> = {
|
|
|
30
31
|
// ── Field ─────────────────────────────────────────────────────────────────────
|
|
31
32
|
|
|
32
33
|
export type FormFieldType =
|
|
33
|
-
| "text" | "textarea" | "number" | "date"
|
|
34
|
-
| "select" | "lookup" | "checkbox" | "toggle"
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
| "text" | "textarea" | "number" | "date" | "datetime"
|
|
35
|
+
| "select" | "lookup" | "checkbox" | "toggle"
|
|
36
|
+
| "detail-rows";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Select option. `value` may be a number for numeric-coded columns (e.g.
|
|
40
|
+
* kolektibilitas 1–5); the renderer compares via `String(value)` and the
|
|
41
|
+
* native `<select>` still round-trips strings, so `format`/`onDataPatch`
|
|
42
|
+
* should not assume the runtime type survives an edit.
|
|
43
|
+
*/
|
|
44
|
+
export type FormFieldOption = { value: string | number; label: string };
|
|
37
45
|
|
|
38
46
|
export type FormFieldLookupConfig<TData extends Record<string, unknown>> = {
|
|
39
47
|
lookupAPI: string;
|
|
@@ -45,6 +53,17 @@ export type FormFieldLookupConfig<TData extends Record<string, unknown>> = {
|
|
|
45
53
|
dataSource?: "api" | "direct";
|
|
46
54
|
/** Return a patch to merge into the form when a lookup row is selected. */
|
|
47
55
|
onDataPatch?: (row: Record<string, unknown>, data: TData) => Partial<TData>;
|
|
56
|
+
/**
|
|
57
|
+
* Form field keys whose values combine into the initial display text on
|
|
58
|
+
* Edit load. Example: `["kode_cabang","branch_name"]` renders
|
|
59
|
+
* "001 KANTOR CABANG BANDUNG" instead of just "001". Typical pairing
|
|
60
|
+
* with an onDataPatch that stashes the picked row's name into a
|
|
61
|
+
* companion field. If any referenced field is empty, the lookup falls
|
|
62
|
+
* back to the field's own primary-key value.
|
|
63
|
+
*/
|
|
64
|
+
initialDisplayFields?: (keyof TData)[];
|
|
65
|
+
/** Separator joining initialDisplayFields. Default: " " */
|
|
66
|
+
initialDisplaySeparator?: string;
|
|
48
67
|
};
|
|
49
68
|
|
|
50
69
|
export type FormFieldNumericConfig<TData extends Record<string, unknown>> =
|
|
@@ -53,11 +72,63 @@ export type FormFieldNumericConfig<TData extends Record<string, unknown>> =
|
|
|
53
72
|
| { kind: "integer" }
|
|
54
73
|
| { kind: "phone"; minDigits: number; maxDigits: number; allowedPrefixes?: string[] };
|
|
55
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Toggle field state mapping for non-boolean stored values.
|
|
77
|
+
*
|
|
78
|
+
* Example for a "Y" / "N" string column:
|
|
79
|
+
* { type: "toggle", toggle: { valueOn: "Y", valueOff: "N" } }
|
|
80
|
+
*
|
|
81
|
+
* Toggled = (current value === valueOn). Flipping the toggle writes
|
|
82
|
+
* either `valueOn` or `valueOff` into the form data. Defaults to
|
|
83
|
+
* boolean true / false when omitted.
|
|
84
|
+
*/
|
|
85
|
+
export type FormFieldToggleConfig = {
|
|
86
|
+
valueOn?: unknown;
|
|
87
|
+
valueOff?: unknown;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Detail-rows config — embeds an inline `<EditableTable>` inside the form for
|
|
92
|
+
* master-detail entry. The field value at `key` must be an array of plain
|
|
93
|
+
* objects (e.g. `details: BucketRow[]`). The renderer reads / writes that
|
|
94
|
+
* array atomically via `onChange({ ...data, [key]: newRows })`.
|
|
95
|
+
*
|
|
96
|
+
* `columns`, `newRowFactory`, `validateRow`, `maxRows` mirror the props of
|
|
97
|
+
* `<EditableTable>` from `@isi-ui7/editable-table`.
|
|
98
|
+
*/
|
|
99
|
+
export type DetailRowsConfig = {
|
|
100
|
+
columns: EditableColumnDef[];
|
|
101
|
+
newRowFactory: () => Record<string, unknown>;
|
|
102
|
+
validateRow?: (
|
|
103
|
+
row: Record<string, unknown>,
|
|
104
|
+
index: number,
|
|
105
|
+
) => Record<string, string>;
|
|
106
|
+
maxRows?: number;
|
|
107
|
+
/**
|
|
108
|
+
* Assignment/master-detail mode: rows loaded from the server render
|
|
109
|
+
* delete-only (read-only cells, no lookup picker) while rows added in the
|
|
110
|
+
* current session stay editable. Defaults to `false` (normal edit-in-place).
|
|
111
|
+
* Forwarded to `<EditableTable lockExistingRows>`.
|
|
112
|
+
*/
|
|
113
|
+
lockExistingRows?: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
56
116
|
export type FormField<TData extends Record<string, unknown>> = {
|
|
57
117
|
key: keyof TData;
|
|
58
118
|
label: ReactNode;
|
|
59
119
|
type: FormFieldType;
|
|
60
120
|
span?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Force this field to start a new row even when the current row still
|
|
123
|
+
* has space for its `span`. Useful to break a logical group onto its
|
|
124
|
+
* own line (e.g. a wide picker that should sit alone, or a sub-group
|
|
125
|
+
* header pattern within one section).
|
|
126
|
+
*
|
|
127
|
+
* Implemented via `gridColumnStart: 1` — the CSS grid skips the
|
|
128
|
+
* remaining columns of the previous row and places this field at the
|
|
129
|
+
* left edge of a new row.
|
|
130
|
+
*/
|
|
131
|
+
breakBefore?: boolean;
|
|
61
132
|
/** Static or dynamic readonly. Function receives current mode + form data. */
|
|
62
133
|
readonly?: boolean | ((mode: FormMode, data: TData) => boolean);
|
|
63
134
|
/** Static or dynamic visibility. Hidden fields are skipped in validation. */
|
|
@@ -72,6 +143,8 @@ export type FormField<TData extends Record<string, unknown>> = {
|
|
|
72
143
|
options?: FormFieldOption[];
|
|
73
144
|
lookup?: FormFieldLookupConfig<TData>;
|
|
74
145
|
numeric?: FormFieldNumericConfig<TData>;
|
|
146
|
+
toggle?: FormFieldToggleConfig;
|
|
147
|
+
detailRows?: DetailRowsConfig;
|
|
75
148
|
format?: (value: TData[keyof TData], data: TData) => ReactNode;
|
|
76
149
|
validation?: FieldValidation<TData>;
|
|
77
150
|
};
|
|
@@ -111,6 +184,11 @@ export type CrudForm<TData extends Record<string, unknown>> = {
|
|
|
111
184
|
backLabel?: { view?: string; others?: string};
|
|
112
185
|
/** Form layout density. Defaults to "compact". */
|
|
113
186
|
density?: Ui7FormDensity;
|
|
187
|
+
/**
|
|
188
|
+
* Desktop form-panel width — "half" | "two-thirds" | "full".
|
|
189
|
+
* Default "two-thirds". Tablet + mobile (<1056px) always stretch full.
|
|
190
|
+
*/
|
|
191
|
+
width?: Ui7FormWidth;
|
|
114
192
|
|
|
115
193
|
emptyData: TData;
|
|
116
194
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildValueSections, type JSONSchema } from "./form-value-schema";
|
|
3
|
+
import type { FormFieldNumericConfig } from "./form-types";
|
|
4
|
+
|
|
5
|
+
// Mock translate: prefer the provided fallback, else echo the key.
|
|
6
|
+
const t = (key: string, fallback?: string) => fallback ?? key;
|
|
7
|
+
|
|
8
|
+
type ValueData = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
describe("buildValueSections — transaction_limit (two-limit + x-rules)", () => {
|
|
11
|
+
const schema: JSONSchema = {
|
|
12
|
+
type: "object",
|
|
13
|
+
required: ["transaction_limit", "authorization_limit", "currency"],
|
|
14
|
+
properties: {
|
|
15
|
+
transaction_limit: {
|
|
16
|
+
type: "number",
|
|
17
|
+
minimum: 0,
|
|
18
|
+
"x-ui": {
|
|
19
|
+
labelKey: "policy.value.txnLimit",
|
|
20
|
+
numeric: { kind: "currency", currencyField: "currency" },
|
|
21
|
+
span: 6,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
authorization_limit: {
|
|
25
|
+
type: "number",
|
|
26
|
+
minimum: 0,
|
|
27
|
+
"x-ui": {
|
|
28
|
+
labelKey: "policy.value.authLimit",
|
|
29
|
+
numeric: { kind: "currency", currencyField: "currency" },
|
|
30
|
+
span: 6,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
currency: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: ["IDR"],
|
|
36
|
+
default: "IDR",
|
|
37
|
+
"x-ui": { widget: "select", labelKey: "policy.value.currency", span: 6 },
|
|
38
|
+
},
|
|
39
|
+
scope: {
|
|
40
|
+
type: "string",
|
|
41
|
+
enum: ["per_transaction", "per_day", "per_month"],
|
|
42
|
+
"x-ui": { widget: "select", labelKey: "policy.value.scope", span: 6 },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
"x-ui": {
|
|
46
|
+
layout: {
|
|
47
|
+
sections: [
|
|
48
|
+
{ titleKey: "policy.section.limits", fields: ["transaction_limit", "authorization_limit"] },
|
|
49
|
+
{ titleKey: "policy.section.meta", fields: ["currency", "scope"] },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"x-rules": [
|
|
54
|
+
{
|
|
55
|
+
op: "lte",
|
|
56
|
+
left: "authorization_limit",
|
|
57
|
+
right: "transaction_limit",
|
|
58
|
+
message: "Authorization limit must be ≤ transaction limit",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
it("groups properties into the two layout sections", () => {
|
|
64
|
+
const { sections } = buildValueSections(schema, t);
|
|
65
|
+
expect(sections).toHaveLength(2);
|
|
66
|
+
expect(sections[0].title).toBe("policy.section.limits");
|
|
67
|
+
expect(sections[0].fields.map((f) => f.key)).toEqual([
|
|
68
|
+
"transaction_limit",
|
|
69
|
+
"authorization_limit",
|
|
70
|
+
]);
|
|
71
|
+
expect(sections[1].title).toBe("policy.section.meta");
|
|
72
|
+
expect(sections[1].fields.map((f) => f.key)).toEqual(["currency", "scope"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("renders the two limits as side-by-side currency fields", () => {
|
|
76
|
+
const { sections } = buildValueSections(schema, t);
|
|
77
|
+
const [txn, auth] = sections[0].fields;
|
|
78
|
+
expect(txn.type).toBe("number");
|
|
79
|
+
expect(txn.span).toBe(6);
|
|
80
|
+
const num = txn.numeric as Extract<FormFieldNumericConfig<ValueData>, { kind: "currency" }>;
|
|
81
|
+
expect(num.kind).toBe("currency");
|
|
82
|
+
expect(num.currencyField).toBe("currency");
|
|
83
|
+
// precision defaults applied when x-ui omits them
|
|
84
|
+
expect(num.precisionByCurrency).toMatchObject({ IDR: 2 });
|
|
85
|
+
expect(auth.numeric).toMatchObject({ kind: "currency", currencyField: "currency" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("maps required + minimum into per-field validation", () => {
|
|
89
|
+
const { sections } = buildValueSections(schema, t);
|
|
90
|
+
const txn = sections[0].fields[0];
|
|
91
|
+
expect(txn.validation).toMatchObject({ required: true, min: 0 });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("renders currency/scope as selects with enum options", () => {
|
|
95
|
+
const { sections } = buildValueSections(schema, t);
|
|
96
|
+
const [currency, scope] = sections[1].fields;
|
|
97
|
+
expect(currency.type).toBe("select");
|
|
98
|
+
expect(currency.options).toEqual([{ value: "IDR", label: "IDR" }]);
|
|
99
|
+
expect(scope.options?.map((o) => o.value)).toEqual([
|
|
100
|
+
"per_transaction",
|
|
101
|
+
"per_day",
|
|
102
|
+
"per_month",
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("produces an lte cross-field validator that blocks an over-limit value", () => {
|
|
107
|
+
const { validate } = buildValueSections(schema, t);
|
|
108
|
+
expect(validate({ transaction_limit: 1000, authorization_limit: 2000 })).toEqual({
|
|
109
|
+
authorization_limit: "Authorization limit must be ≤ transaction limit",
|
|
110
|
+
});
|
|
111
|
+
// valid case: auth ≤ txn → no error
|
|
112
|
+
expect(validate({ transaction_limit: 2000, authorization_limit: 1000 })).toEqual({});
|
|
113
|
+
// incomplete input → skip (per-field required handles emptiness)
|
|
114
|
+
expect(validate({ transaction_limit: 1000 })).toEqual({});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("buildValueSections — rate", () => {
|
|
119
|
+
const schema: JSONSchema = {
|
|
120
|
+
type: "object",
|
|
121
|
+
required: ["rate", "rate_unit"],
|
|
122
|
+
properties: {
|
|
123
|
+
rate: { type: "number", minimum: 0, "x-ui": { numeric: { kind: "percent", maxFractionDigits: 4 }, span: 6 } },
|
|
124
|
+
rate_unit: {
|
|
125
|
+
type: "string",
|
|
126
|
+
enum: ["percent_per_year", "percent_per_month"],
|
|
127
|
+
"x-ui": { widget: "select", span: 6 },
|
|
128
|
+
},
|
|
129
|
+
tenor_months: { type: "integer", minimum: 0, "x-ui": { numeric: { kind: "integer" }, span: 6 } },
|
|
130
|
+
calculation_method: {
|
|
131
|
+
type: "string",
|
|
132
|
+
enum: ["simple_interest", "compound"],
|
|
133
|
+
"x-ui": { widget: "select", span: 6 },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
it("falls back to a single 'Value' section ordered by declaration", () => {
|
|
139
|
+
const { sections } = buildValueSections(schema, t);
|
|
140
|
+
expect(sections).toHaveLength(1);
|
|
141
|
+
expect(sections[0].title).toBe("Value");
|
|
142
|
+
expect(sections[0].fields.map((f) => f.key)).toEqual([
|
|
143
|
+
"rate",
|
|
144
|
+
"rate_unit",
|
|
145
|
+
"tenor_months",
|
|
146
|
+
"calculation_method",
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("maps percent + integer numeric kinds", () => {
|
|
151
|
+
const { sections } = buildValueSections(schema, t);
|
|
152
|
+
const [rate, , tenor] = sections[0].fields;
|
|
153
|
+
expect(rate.numeric).toEqual({ kind: "percent", maxFractionDigits: 4 });
|
|
154
|
+
expect(tenor.numeric).toEqual({ kind: "integer" });
|
|
155
|
+
expect(tenor.inputMode).toBe("numeric");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("has no x-rules → validator returns no errors", () => {
|
|
159
|
+
const { validate } = buildValueSections(schema, t);
|
|
160
|
+
expect(validate({ rate: 5, rate_unit: "percent_per_year" })).toEqual({});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("buildValueSections — operational_hours", () => {
|
|
165
|
+
const schema: JSONSchema = {
|
|
166
|
+
type: "object",
|
|
167
|
+
required: ["day_of_week"],
|
|
168
|
+
properties: {
|
|
169
|
+
day_of_week: {
|
|
170
|
+
type: "string",
|
|
171
|
+
enum: ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"],
|
|
172
|
+
"x-ui": { widget: "select", span: 4 },
|
|
173
|
+
},
|
|
174
|
+
is_open: { type: "boolean", "x-ui": { widget: "toggle", span: 4 } },
|
|
175
|
+
open_time: { type: "string", pattern: "^[0-2][0-9]:[0-5][0-9]$", "x-ui": { placeholder: "HH:MM", span: 4 } },
|
|
176
|
+
close_time: { type: "string", pattern: "^[0-2][0-9]:[0-5][0-9]$", "x-ui": { placeholder: "HH:MM", span: 4 } },
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
it("infers toggle, select, and pattern-validated text fields", () => {
|
|
181
|
+
const { sections } = buildValueSections(schema, t);
|
|
182
|
+
const fields = sections[0].fields;
|
|
183
|
+
const byKey = Object.fromEntries(fields.map((f) => [f.key, f]));
|
|
184
|
+
expect(byKey.day_of_week.type).toBe("select");
|
|
185
|
+
expect(byKey.is_open.type).toBe("toggle");
|
|
186
|
+
expect(byKey.open_time.type).toBe("text");
|
|
187
|
+
expect(byKey.open_time.placeholder).toBe("HH:MM");
|
|
188
|
+
expect((byKey.open_time.validation?.pattern as RegExp).source).toBe("^[0-2][0-9]:[0-5][0-9]$");
|
|
189
|
+
expect(byKey.day_of_week.validation?.required).toBe(true);
|
|
190
|
+
expect(byKey.is_open.validation).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("buildValueSections — tiered array → detail-rows", () => {
|
|
195
|
+
const schema: JSONSchema = {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
tiers: {
|
|
199
|
+
type: "array",
|
|
200
|
+
items: {
|
|
201
|
+
type: "object",
|
|
202
|
+
required: ["tenor_months", "rate"],
|
|
203
|
+
properties: {
|
|
204
|
+
tenor_months: { type: "integer", "x-ui": { numeric: { kind: "integer" } } },
|
|
205
|
+
rate: { type: "number", "x-ui": { numeric: { kind: "percent", maxFractionDigits: 4 } } },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
"x-ui": { widget: "detail-rows", labelKey: "policy.value.tiers", maxRows: 20, span: 12 },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
it("maps an array-of-object property to a detail-rows field", () => {
|
|
214
|
+
const { sections } = buildValueSections(schema, t);
|
|
215
|
+
const field = sections[0].fields[0];
|
|
216
|
+
expect(field.type).toBe("detail-rows");
|
|
217
|
+
expect(field.span).toBe(12);
|
|
218
|
+
expect(field.label).toBe("tiers"); // mock t() returns the fallback (field name)
|
|
219
|
+
expect(field.detailRows?.maxRows).toBe(20);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("derives columns + types from items.properties", () => {
|
|
223
|
+
const { sections } = buildValueSections(schema, t);
|
|
224
|
+
const detail = sections[0].fields[0].detailRows!;
|
|
225
|
+
expect(detail.columns.map((c) => c.field)).toEqual(["tenor_months", "rate"]);
|
|
226
|
+
expect(detail.columns[0].columnType).toMatchObject({ type: "number", decimals: 0 });
|
|
227
|
+
expect(detail.columns[1].columnType).toMatchObject({ type: "number", decimals: 4 });
|
|
228
|
+
expect(detail.columns.every((c) => c.required)).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("newRowFactory seeds typed defaults; validateRow flags missing required", () => {
|
|
232
|
+
const { sections } = buildValueSections(schema, t);
|
|
233
|
+
const detail = sections[0].fields[0].detailRows!;
|
|
234
|
+
expect(detail.newRowFactory()).toEqual({ tenor_months: 0, rate: 0 });
|
|
235
|
+
expect(detail.validateRow?.({ tenor_months: "", rate: 5 }, 0)).toHaveProperty("tenor_months");
|
|
236
|
+
expect(detail.validateRow?.({ tenor_months: 12, rate: 5 }, 0)).toEqual({});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("buildXRulesValidator op coverage", () => {
|
|
241
|
+
it("handles gte / lt / gt / eq / required-if", () => {
|
|
242
|
+
const mk = (rule: NonNullable<JSONSchema["x-rules"]>[number]) =>
|
|
243
|
+
buildValueSections({ type: "object", properties: {}, "x-rules": [rule] }, t).validate;
|
|
244
|
+
|
|
245
|
+
expect(mk({ op: "gte", left: "a", right: "b", message: "ge" })({ a: 1, b: 2 })).toEqual({ a: "ge" });
|
|
246
|
+
expect(mk({ op: "lt", left: "a", value: 10, message: "lt" })({ a: 10 })).toEqual({ a: "lt" });
|
|
247
|
+
expect(mk({ op: "gt", left: "a", value: 0, message: "gt" })({ a: 0 })).toEqual({ a: "gt" });
|
|
248
|
+
expect(mk({ op: "eq", left: "a", value: "IDR", message: "eq" })({ a: "USD" })).toEqual({ a: "eq" });
|
|
249
|
+
expect(
|
|
250
|
+
mk({ op: "required-if", left: "reason", right: "kind", value: "OTHER", message: "req" })({
|
|
251
|
+
kind: "OTHER",
|
|
252
|
+
reason: "",
|
|
253
|
+
}),
|
|
254
|
+
).toEqual({ reason: "req" });
|
|
255
|
+
// condition not met → no error
|
|
256
|
+
expect(
|
|
257
|
+
mk({ op: "required-if", left: "reason", right: "kind", value: "OTHER", message: "req" })({
|
|
258
|
+
kind: "NORMAL",
|
|
259
|
+
reason: "",
|
|
260
|
+
}),
|
|
261
|
+
).toEqual({});
|
|
262
|
+
});
|
|
263
|
+
});
|