@webiny/app-admin 6.6.0-alpha.7 → 6.6.0-alpha.9
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/base/Base/FieldRenderers/AutoCompleteRenderer.js +1 -0
- package/base/Base/FieldRenderers/AutoCompleteRenderer.js.map +1 -1
- package/base/Base/FieldRenderers/RadioButtonsRenderer.js +3 -2
- package/base/Base/FieldRenderers/RadioButtonsRenderer.js.map +1 -1
- package/base/Base/FieldRenderers/SelectRenderer.js +4 -2
- package/base/Base/FieldRenderers/SelectRenderer.js.map +1 -1
- package/features/formModel/AccessControlRuleEvaluator.d.ts +12 -0
- package/features/formModel/AccessControlRuleEvaluator.js +30 -0
- package/features/formModel/AccessControlRuleEvaluator.js.map +1 -0
- package/features/formModel/AccessControlRuleEvaluator.test.d.ts +1 -0
- package/features/formModel/AccessControlRuleEvaluator.test.js +219 -0
- package/features/formModel/AccessControlRuleEvaluator.test.js.map +1 -0
- package/features/formModel/ConditionRuleEvaluator.js +34 -4
- package/features/formModel/ConditionRuleEvaluator.js.map +1 -1
- package/features/formModel/Field.d.ts +1 -0
- package/features/formModel/Field.js +16 -0
- package/features/formModel/Field.js.map +1 -1
- package/features/formModel/FieldBuilder.test.js +2 -0
- package/features/formModel/FieldBuilder.test.js.map +1 -1
- package/features/formModel/FormModel.test.js +248 -2
- package/features/formModel/FormModel.test.js.map +1 -1
- package/features/formModel/ObjectField.d.ts +1 -0
- package/features/formModel/ObjectField.js +24 -0
- package/features/formModel/ObjectField.js.map +1 -1
- package/features/formModel/Rules.test.js +169 -16
- package/features/formModel/Rules.test.js.map +1 -1
- package/features/formModel/abstractions.d.ts +2 -2
- package/features/formModel/abstractions.js.map +1 -1
- package/features/formModel/feature.js +2 -0
- package/features/formModel/feature.js.map +1 -1
- package/package.json +20 -20
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base/Base/FieldRenderers/AutoCompleteRenderer.js","sources":["../../../../src/base/Base/FieldRenderers/AutoCompleteRenderer.tsx"],"sourcesContent":["import React from \"react\";\nimport { createFieldRenderer } from \"~/features/formModel/createFieldRenderer.js\";\nimport { AutoComplete } from \"@webiny/admin-ui\";\nimport type { IValueOption } from \"~/features/formModel/index.js\";\n\ndeclare module \"../../../features/formModel/abstractions.js\" {\n interface IFieldRendererRegistry {\n autoComplete: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n }\n}\n\nexport const AutoCompleteRenderer = createFieldRenderer(({ field }) => {\n const options: IValueOption[] = field.options ?? [];\n\n return (\n <AutoComplete\n label={field.label}\n hint={field.help}\n placeholder={field.placeholder}\n description={field.description}\n note={field.note}\n value={field.value != null ? String(field.value) : \"\"}\n onValueChange={value => {\n field.onChange(value);\n field.onBlur();\n }}\n required={field.required}\n disabled={field.disabled}\n validation={field.validation}\n options={options.map(opt => ({\n label: opt.label,\n value: String(opt.value),\n disabled: opt.disabled\n }))}\n />\n );\n});\n"],"names":["AutoCompleteRenderer","createFieldRenderer","field","options","AutoComplete","String","value","opt"],"mappings":";;;AAWO,MAAMA,uBAAuBC,oBAAoB,CAAC,EAAEC,KAAK,EAAE;IAC9D,MAAMC,UAA0BD,MAAM,OAAO,IAAI,EAAE;IAEnD,OAAO,WAAP,GACI,oBAACE,cAAYA;QACT,OAAOF,MAAM,KAAK;QAClB,MAAMA,MAAM,IAAI;QAChB,aAAaA,MAAM,WAAW;QAC9B,aAAaA,MAAM,WAAW;QAC9B,MAAMA,MAAM,IAAI;QAChB,OAAOA,AAAe,QAAfA,MAAM,KAAK,GAAWG,OAAOH,MAAM,KAAK,IAAI;QACnD,eAAeI,CAAAA;YACXJ,MAAM,QAAQ,CAACI;YACfJ,MAAM,MAAM;QAChB;QACA,UAAUA,MAAM,QAAQ;
|
|
1
|
+
{"version":3,"file":"base/Base/FieldRenderers/AutoCompleteRenderer.js","sources":["../../../../src/base/Base/FieldRenderers/AutoCompleteRenderer.tsx"],"sourcesContent":["import React from \"react\";\nimport { createFieldRenderer } from \"~/features/formModel/createFieldRenderer.js\";\nimport { AutoComplete } from \"@webiny/admin-ui\";\nimport type { IValueOption } from \"~/features/formModel/index.js\";\n\ndeclare module \"../../../features/formModel/abstractions.js\" {\n interface IFieldRendererRegistry {\n autoComplete: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n }\n}\n\nexport const AutoCompleteRenderer = createFieldRenderer(({ field }) => {\n const options: IValueOption[] = field.options ?? [];\n\n return (\n <AutoComplete\n label={field.label}\n hint={field.help}\n placeholder={field.placeholder}\n description={field.description}\n note={field.note}\n value={field.value != null ? String(field.value) : \"\"}\n onValueChange={value => {\n field.onChange(value);\n field.onBlur();\n }}\n required={field.required}\n // Clearing a required field can only leave it invalid, so it gets no reset action.\n displayResetAction={!field.required}\n disabled={field.disabled}\n validation={field.validation}\n options={options.map(opt => ({\n label: opt.label,\n value: String(opt.value),\n disabled: opt.disabled\n }))}\n />\n );\n});\n"],"names":["AutoCompleteRenderer","createFieldRenderer","field","options","AutoComplete","String","value","opt"],"mappings":";;;AAWO,MAAMA,uBAAuBC,oBAAoB,CAAC,EAAEC,KAAK,EAAE;IAC9D,MAAMC,UAA0BD,MAAM,OAAO,IAAI,EAAE;IAEnD,OAAO,WAAP,GACI,oBAACE,cAAYA;QACT,OAAOF,MAAM,KAAK;QAClB,MAAMA,MAAM,IAAI;QAChB,aAAaA,MAAM,WAAW;QAC9B,aAAaA,MAAM,WAAW;QAC9B,MAAMA,MAAM,IAAI;QAChB,OAAOA,AAAe,QAAfA,MAAM,KAAK,GAAWG,OAAOH,MAAM,KAAK,IAAI;QACnD,eAAeI,CAAAA;YACXJ,MAAM,QAAQ,CAACI;YACfJ,MAAM,MAAM;QAChB;QACA,UAAUA,MAAM,QAAQ;QAExB,oBAAoB,CAACA,MAAM,QAAQ;QACnC,UAAUA,MAAM,QAAQ;QACxB,YAAYA,MAAM,UAAU;QAC5B,SAASC,QAAQ,GAAG,CAACI,CAAAA,MAAQ;gBACzB,OAAOA,IAAI,KAAK;gBAChB,OAAOF,OAAOE,IAAI,KAAK;gBACvB,UAAUA,IAAI,QAAQ;YAC1B;;AAGZ"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import react from "react";
|
|
2
2
|
import { createFieldRenderer } from "../../../features/formModel/createFieldRenderer.js";
|
|
3
3
|
import { RadioGroup } from "@webiny/admin-ui";
|
|
4
|
+
const isValidValue = (value)=>"string" == typeof value || "number" == typeof value;
|
|
4
5
|
const RadioButtonsRenderer = createFieldRenderer(({ field })=>{
|
|
5
6
|
const options = field.options ?? [];
|
|
6
7
|
return /*#__PURE__*/ react.createElement(RadioGroup, {
|
|
@@ -13,9 +14,9 @@ const RadioButtonsRenderer = createFieldRenderer(({ field })=>{
|
|
|
13
14
|
validation: field.validation,
|
|
14
15
|
items: options.map((opt)=>({
|
|
15
16
|
label: opt.label,
|
|
16
|
-
value: String(opt.value)
|
|
17
|
+
value: isValidValue(opt.value) ? String(opt.value) : opt.value
|
|
17
18
|
})),
|
|
18
|
-
value:
|
|
19
|
+
value: isValidValue(field.value) ? String(field.value) : void 0,
|
|
19
20
|
onChange: (value)=>{
|
|
20
21
|
field.onChange(value);
|
|
21
22
|
field.onBlur();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base/Base/FieldRenderers/RadioButtonsRenderer.js","sources":["../../../../src/base/Base/FieldRenderers/RadioButtonsRenderer.tsx"],"sourcesContent":["import React from \"react\";\nimport { createFieldRenderer } from \"~/features/formModel/createFieldRenderer.js\";\nimport { RadioGroup } from \"@webiny/admin-ui\";\nimport type { IValueOption } from \"~/features/formModel/index.js\";\n\ndeclare module \"../../../features/formModel/abstractions.js\" {\n interface IFieldRendererRegistry {\n radioButtons: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n }\n}\n\nexport const RadioButtonsRenderer = createFieldRenderer(({ field }) => {\n const options: IValueOption[] = field.options ?? [];\n\n return (\n <RadioGroup\n label={field.label}\n hint={field.help}\n description={field.description}\n note={field.note}\n required={field.required}\n disabled={field.disabled}\n validation={field.validation}\n items={options.map(opt => ({\n label: opt.label,\n value: String(opt.value)\n }))}\n value={field.value
|
|
1
|
+
{"version":3,"file":"base/Base/FieldRenderers/RadioButtonsRenderer.js","sources":["../../../../src/base/Base/FieldRenderers/RadioButtonsRenderer.tsx"],"sourcesContent":["import React from \"react\";\nimport { createFieldRenderer } from \"~/features/formModel/createFieldRenderer.js\";\nimport { RadioGroup } from \"@webiny/admin-ui\";\nimport type { IValueOption } from \"~/features/formModel/index.js\";\n\ndeclare module \"../../../features/formModel/abstractions.js\" {\n interface IFieldRendererRegistry {\n radioButtons: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n }\n}\n\nconst isValidValue = (value: unknown): value is string | number =>\n typeof value === \"string\" || typeof value === \"number\";\n\nexport const RadioButtonsRenderer = createFieldRenderer(({ field }) => {\n const options: IValueOption[] = field.options ?? [];\n\n return (\n <RadioGroup\n label={field.label}\n hint={field.help}\n description={field.description}\n note={field.note}\n required={field.required}\n disabled={field.disabled}\n validation={field.validation}\n items={options.map(opt => ({\n label: opt.label,\n value: isValidValue(opt.value) ? String(opt.value) : opt.value\n }))}\n value={isValidValue(field.value) ? String(field.value) : undefined}\n onChange={value => {\n field.onChange(value);\n field.onBlur();\n }}\n />\n );\n});\n"],"names":["isValidValue","value","RadioButtonsRenderer","createFieldRenderer","field","options","RadioGroup","opt","String","undefined"],"mappings":";;;AAWA,MAAMA,eAAe,CAACC,QAClB,AAAiB,YAAjB,OAAOA,SAAsB,AAAiB,YAAjB,OAAOA;AAEjC,MAAMC,uBAAuBC,oBAAoB,CAAC,EAAEC,KAAK,EAAE;IAC9D,MAAMC,UAA0BD,MAAM,OAAO,IAAI,EAAE;IAEnD,OAAO,WAAP,GACI,oBAACE,YAAUA;QACP,OAAOF,MAAM,KAAK;QAClB,MAAMA,MAAM,IAAI;QAChB,aAAaA,MAAM,WAAW;QAC9B,MAAMA,MAAM,IAAI;QAChB,UAAUA,MAAM,QAAQ;QACxB,UAAUA,MAAM,QAAQ;QACxB,YAAYA,MAAM,UAAU;QAC5B,OAAOC,QAAQ,GAAG,CAACE,CAAAA,MAAQ;gBACvB,OAAOA,IAAI,KAAK;gBAChB,OAAOP,aAAaO,IAAI,KAAK,IAAIC,OAAOD,IAAI,KAAK,IAAIA,IAAI,KAAK;YAClE;QACA,OAAOP,aAAaI,MAAM,KAAK,IAAII,OAAOJ,MAAM,KAAK,IAAIK;QACzD,UAAUR,CAAAA;YACNG,MAAM,QAAQ,CAACH;YACfG,MAAM,MAAM;QAChB;;AAGZ"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import react from "react";
|
|
2
2
|
import { createFieldRenderer } from "../../../features/formModel/createFieldRenderer.js";
|
|
3
3
|
import { Select } from "@webiny/admin-ui";
|
|
4
|
+
const isValidValue = (value)=>"string" == typeof value || "number" == typeof value;
|
|
4
5
|
const SelectRenderer = createFieldRenderer(({ field })=>{
|
|
5
6
|
const options = field.options ?? [];
|
|
6
7
|
return /*#__PURE__*/ react.createElement(Select, {
|
|
@@ -9,17 +10,18 @@ const SelectRenderer = createFieldRenderer(({ field })=>{
|
|
|
9
10
|
placeholder: field.placeholder,
|
|
10
11
|
description: field.description,
|
|
11
12
|
note: field.note,
|
|
12
|
-
value:
|
|
13
|
+
value: isValidValue(field.value) ? String(field.value) : "",
|
|
13
14
|
onChange: (value)=>{
|
|
14
15
|
field.onChange(value);
|
|
15
16
|
field.onBlur();
|
|
16
17
|
},
|
|
17
18
|
required: field.required,
|
|
19
|
+
displayResetAction: !field.required,
|
|
18
20
|
disabled: field.disabled,
|
|
19
21
|
validation: field.validation,
|
|
20
22
|
options: options.map((opt)=>({
|
|
21
23
|
label: opt.label,
|
|
22
|
-
value: String(opt.value),
|
|
24
|
+
value: isValidValue(opt.value) ? String(opt.value) : opt.value,
|
|
23
25
|
disabled: opt.disabled
|
|
24
26
|
}))
|
|
25
27
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base/Base/FieldRenderers/SelectRenderer.js","sources":["../../../../src/base/Base/FieldRenderers/SelectRenderer.tsx"],"sourcesContent":["import React from \"react\";\nimport { createFieldRenderer } from \"~/features/formModel/createFieldRenderer.js\";\nimport { Select } from \"@webiny/admin-ui\";\nimport type { IValueOption } from \"~/features/formModel/index.js\";\n\ndeclare module \"../../../features/formModel/abstractions.js\" {\n interface IFieldRendererRegistry {\n select: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n /** @deprecated Use \"select\" instead. */\n dropdown: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n }\n}\n\nexport const SelectRenderer = createFieldRenderer(({ field }) => {\n const options: IValueOption[] = field.options ?? [];\n\n return (\n <Select\n label={field.label}\n hint={field.help}\n placeholder={field.placeholder}\n description={field.description}\n note={field.note}\n value={field.value
|
|
1
|
+
{"version":3,"file":"base/Base/FieldRenderers/SelectRenderer.js","sources":["../../../../src/base/Base/FieldRenderers/SelectRenderer.tsx"],"sourcesContent":["import React from \"react\";\nimport { createFieldRenderer } from \"~/features/formModel/createFieldRenderer.js\";\nimport { Select } from \"@webiny/admin-ui\";\nimport type { IValueOption } from \"~/features/formModel/index.js\";\n\ndeclare module \"../../../features/formModel/abstractions.js\" {\n interface IFieldRendererRegistry {\n select: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n /** @deprecated Use \"select\" instead. */\n dropdown: { fieldType: \"text\" | \"number\"; options: true; settings: undefined };\n }\n}\n\nconst isValidValue = (value: unknown): value is string | number =>\n typeof value === \"string\" || typeof value === \"number\";\n\nexport const SelectRenderer = createFieldRenderer(({ field }) => {\n const options: IValueOption[] = field.options ?? [];\n\n return (\n <Select\n label={field.label}\n hint={field.help}\n placeholder={field.placeholder}\n description={field.description}\n note={field.note}\n value={isValidValue(field.value) ? String(field.value) : \"\"}\n onChange={value => {\n field.onChange(value);\n field.onBlur();\n }}\n required={field.required}\n // Clearing a required field can only leave it invalid, so it gets no reset action.\n displayResetAction={!field.required}\n disabled={field.disabled}\n validation={field.validation}\n options={options.map(opt => ({\n label: opt.label,\n value: isValidValue(opt.value) ? String(opt.value) : opt.value,\n disabled: opt.disabled\n }))}\n />\n );\n});\n"],"names":["isValidValue","value","SelectRenderer","createFieldRenderer","field","options","Select","String","opt"],"mappings":";;;AAaA,MAAMA,eAAe,CAACC,QAClB,AAAiB,YAAjB,OAAOA,SAAsB,AAAiB,YAAjB,OAAOA;AAEjC,MAAMC,iBAAiBC,oBAAoB,CAAC,EAAEC,KAAK,EAAE;IACxD,MAAMC,UAA0BD,MAAM,OAAO,IAAI,EAAE;IAEnD,OAAO,WAAP,GACI,oBAACE,QAAMA;QACH,OAAOF,MAAM,KAAK;QAClB,MAAMA,MAAM,IAAI;QAChB,aAAaA,MAAM,WAAW;QAC9B,aAAaA,MAAM,WAAW;QAC9B,MAAMA,MAAM,IAAI;QAChB,OAAOJ,aAAaI,MAAM,KAAK,IAAIG,OAAOH,MAAM,KAAK,IAAI;QACzD,UAAUH,CAAAA;YACNG,MAAM,QAAQ,CAACH;YACfG,MAAM,MAAM;QAChB;QACA,UAAUA,MAAM,QAAQ;QAExB,oBAAoB,CAACA,MAAM,QAAQ;QACnC,UAAUA,MAAM,QAAQ;QACxB,YAAYA,MAAM,UAAU;QAC5B,SAASC,QAAQ,GAAG,CAACG,CAAAA,MAAQ;gBACzB,OAAOA,IAAI,KAAK;gBAChB,OAAOR,aAAaQ,IAAI,KAAK,IAAID,OAAOC,IAAI,KAAK,IAAIA,IAAI,KAAK;gBAC9D,UAAUA,IAAI,QAAQ;YAC1B;;AAGZ"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IdentityContext } from "../../features/security/IdentityContext/abstractions.js";
|
|
2
|
+
import { RuleEvaluator } from "./abstractions.js";
|
|
3
|
+
declare class AccessControlRuleEvaluatorImpl implements RuleEvaluator.Interface {
|
|
4
|
+
private identityContext;
|
|
5
|
+
constructor(identityContext: IdentityContext.Interface);
|
|
6
|
+
canEvaluate(rule: RuleEvaluator.Rule): boolean;
|
|
7
|
+
evaluate(rule: RuleEvaluator.Rule): boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const AccessControlRuleEvaluator: typeof AccessControlRuleEvaluatorImpl & {
|
|
10
|
+
__abstraction: import("@webiny/di").Abstraction<import("./abstractions.js").IRuleEvaluator>;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IdentityContext } from "../security/IdentityContext/abstractions.js";
|
|
2
|
+
import { RuleEvaluator } from "./abstractions.js";
|
|
3
|
+
class AccessControlRuleEvaluatorImpl {
|
|
4
|
+
constructor(identityContext){
|
|
5
|
+
this.identityContext = identityContext;
|
|
6
|
+
}
|
|
7
|
+
canEvaluate(rule) {
|
|
8
|
+
return "accessControl" === rule.type;
|
|
9
|
+
}
|
|
10
|
+
evaluate(rule) {
|
|
11
|
+
const identity = this.identityContext.getIdentity();
|
|
12
|
+
if (!identity.isAuthenticated || "string" != typeof rule.value) return false;
|
|
13
|
+
const separator = rule.value.indexOf(":");
|
|
14
|
+
const scope = rule.value.slice(0, separator);
|
|
15
|
+
const id = rule.value.slice(separator + 1);
|
|
16
|
+
if (-1 === separator || !id) return false;
|
|
17
|
+
if ("admin" === scope) return identity.id === id;
|
|
18
|
+
if ("team" === scope) return identity.teams.some((team)=>team.slug === id);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const AccessControlRuleEvaluator = RuleEvaluator.createImplementation({
|
|
23
|
+
implementation: AccessControlRuleEvaluatorImpl,
|
|
24
|
+
dependencies: [
|
|
25
|
+
IdentityContext
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
export { AccessControlRuleEvaluator };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=AccessControlRuleEvaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/formModel/AccessControlRuleEvaluator.js","sources":["../../../src/features/formModel/AccessControlRuleEvaluator.ts"],"sourcesContent":["import { IdentityContext } from \"~/features/security/IdentityContext/abstractions.js\";\nimport { RuleEvaluator } from \"./abstractions.js\";\n\nclass AccessControlRuleEvaluatorImpl implements RuleEvaluator.Interface {\n constructor(private identityContext: IdentityContext.Interface) {}\n\n canEvaluate(rule: RuleEvaluator.Rule): boolean {\n return rule.type === \"accessControl\";\n }\n\n // Rule values follow the folder-permission target format: `admin:<userId>` or `team:<teamSlug>`.\n evaluate(rule: RuleEvaluator.Rule): boolean {\n const identity = this.identityContext.getIdentity();\n if (!identity.isAuthenticated || typeof rule.value !== \"string\") {\n return false;\n }\n\n const separator = rule.value.indexOf(\":\");\n const scope = rule.value.slice(0, separator);\n const id = rule.value.slice(separator + 1);\n if (separator === -1 || !id) {\n return false;\n }\n\n if (scope === \"admin\") {\n return identity.id === id;\n }\n if (scope === \"team\") {\n return identity.teams.some(team => team.slug === id);\n }\n return false;\n }\n}\n\nexport const AccessControlRuleEvaluator = RuleEvaluator.createImplementation({\n implementation: AccessControlRuleEvaluatorImpl,\n dependencies: [IdentityContext]\n});\n"],"names":["AccessControlRuleEvaluatorImpl","identityContext","rule","identity","separator","scope","id","team","AccessControlRuleEvaluator","RuleEvaluator","IdentityContext"],"mappings":";;AAGA,MAAMA;IACF,YAAoBC,eAA0C,CAAE;aAA5CA,eAAe,GAAfA;IAA6C;IAEjE,YAAYC,IAAwB,EAAW;QAC3C,OAAOA,AAAc,oBAAdA,KAAK,IAAI;IACpB;IAGA,SAASA,IAAwB,EAAW;QACxC,MAAMC,WAAW,IAAI,CAAC,eAAe,CAAC,WAAW;QACjD,IAAI,CAACA,SAAS,eAAe,IAAI,AAAsB,YAAtB,OAAOD,KAAK,KAAK,EAC9C,OAAO;QAGX,MAAME,YAAYF,KAAK,KAAK,CAAC,OAAO,CAAC;QACrC,MAAMG,QAAQH,KAAK,KAAK,CAAC,KAAK,CAAC,GAAGE;QAClC,MAAME,KAAKJ,KAAK,KAAK,CAAC,KAAK,CAACE,YAAY;QACxC,IAAIA,AAAc,OAAdA,aAAoB,CAACE,IACrB,OAAO;QAGX,IAAID,AAAU,YAAVA,OACA,OAAOF,SAAS,EAAE,KAAKG;QAE3B,IAAID,AAAU,WAAVA,OACA,OAAOF,SAAS,KAAK,CAAC,IAAI,CAACI,CAAAA,OAAQA,KAAK,IAAI,KAAKD;QAErD,OAAO;IACX;AACJ;AAEO,MAAME,6BAA6BC,cAAc,oBAAoB,CAAC;IACzE,gBAAgBT;IAChB,cAAc;QAACU;KAAgB;AACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Container } from "@webiny/di";
|
|
3
|
+
import { FormModelFeature } from "./feature.js";
|
|
4
|
+
import { FormModelFactory } from "./abstractions.js";
|
|
5
|
+
import { IdentityContext } from "../security/IdentityContext/abstractions.js";
|
|
6
|
+
import { Identity } from "../../domain/Identity.js";
|
|
7
|
+
const createIdentity = (id, teamSlugs = [])=>Identity.createAuthenticated({
|
|
8
|
+
id,
|
|
9
|
+
type: "admin",
|
|
10
|
+
displayName: id,
|
|
11
|
+
roles: [],
|
|
12
|
+
teams: teamSlugs.map((slug)=>({
|
|
13
|
+
id: `${slug}-id`,
|
|
14
|
+
slug,
|
|
15
|
+
name: slug
|
|
16
|
+
})),
|
|
17
|
+
permissions: [],
|
|
18
|
+
profile: {
|
|
19
|
+
external: false
|
|
20
|
+
},
|
|
21
|
+
currentTenant: {
|
|
22
|
+
id: "root",
|
|
23
|
+
name: "Root"
|
|
24
|
+
},
|
|
25
|
+
defaultTenant: {
|
|
26
|
+
id: "root",
|
|
27
|
+
name: "Root"
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const accessRule = (value, action = "hide")=>({
|
|
31
|
+
type: "accessControl",
|
|
32
|
+
target: "identity",
|
|
33
|
+
operator: "matches",
|
|
34
|
+
value,
|
|
35
|
+
action
|
|
36
|
+
});
|
|
37
|
+
const createContainer = (identity)=>{
|
|
38
|
+
const container = new Container();
|
|
39
|
+
FormModelFeature.register(container);
|
|
40
|
+
container.registerInstance(IdentityContext, {
|
|
41
|
+
getIdentity: ()=>identity,
|
|
42
|
+
setIdentity: ()=>{},
|
|
43
|
+
clear: ()=>{}
|
|
44
|
+
});
|
|
45
|
+
return container;
|
|
46
|
+
};
|
|
47
|
+
const createFieldForm = (identity, rules)=>createContainer(identity).resolve(FormModelFactory).create({
|
|
48
|
+
fields: (fields)=>({
|
|
49
|
+
secret: fields.text().label("Secret").rules(rules)
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
describe("AccessControlRuleEvaluator", ()=>{
|
|
53
|
+
it("hides a field from the admin the rule names", ()=>{
|
|
54
|
+
const form = createFieldForm(createIdentity("user-1"), [
|
|
55
|
+
accessRule("admin:user-1")
|
|
56
|
+
]);
|
|
57
|
+
expect(form.field("secret").visible).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it("leaves the field visible for other admins", ()=>{
|
|
60
|
+
const form = createFieldForm(createIdentity("user-2"), [
|
|
61
|
+
accessRule("admin:user-1")
|
|
62
|
+
]);
|
|
63
|
+
expect(form.field("secret").visible).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it("hides a field from members of the team the rule names, by team slug", ()=>{
|
|
66
|
+
const form = createFieldForm(createIdentity("user-2", [
|
|
67
|
+
"editors"
|
|
68
|
+
]), [
|
|
69
|
+
accessRule("team:editors")
|
|
70
|
+
]);
|
|
71
|
+
expect(form.field("secret").visible).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it("leaves the field visible for admins outside the team", ()=>{
|
|
74
|
+
const form = createFieldForm(createIdentity("user-2", [
|
|
75
|
+
"writers"
|
|
76
|
+
]), [
|
|
77
|
+
accessRule("team:editors")
|
|
78
|
+
]);
|
|
79
|
+
expect(form.field("secret").visible).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it("does not match a team by its id", ()=>{
|
|
82
|
+
const form = createFieldForm(createIdentity("user-2", [
|
|
83
|
+
"editors"
|
|
84
|
+
]), [
|
|
85
|
+
accessRule("team:editors-id")
|
|
86
|
+
]);
|
|
87
|
+
expect(form.field("secret").visible).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
it("disables the field for a viewer rule", ()=>{
|
|
90
|
+
const form = createFieldForm(createIdentity("user-1"), [
|
|
91
|
+
accessRule("admin:user-1", "disable")
|
|
92
|
+
]);
|
|
93
|
+
expect(form.field("secret").visible).toBe(true);
|
|
94
|
+
expect(form.field("secret").vm.disabled).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it("applies when any of several rules matches", ()=>{
|
|
97
|
+
const form = createFieldForm(createIdentity("user-3", [
|
|
98
|
+
"editors"
|
|
99
|
+
]), [
|
|
100
|
+
accessRule("admin:user-1"),
|
|
101
|
+
accessRule("team:editors")
|
|
102
|
+
]);
|
|
103
|
+
expect(form.field("secret").visible).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
describe("read-only and no-access rules on one field", ()=>{
|
|
106
|
+
const rules = [
|
|
107
|
+
accessRule("admin:user-1", "disable"),
|
|
108
|
+
accessRule("team:team-1", "hide")
|
|
109
|
+
];
|
|
110
|
+
it.each([
|
|
111
|
+
[
|
|
112
|
+
"matches only the read-only rule",
|
|
113
|
+
"user-1",
|
|
114
|
+
[],
|
|
115
|
+
true,
|
|
116
|
+
true
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
"matches only the no-access rule",
|
|
120
|
+
"user-2",
|
|
121
|
+
[
|
|
122
|
+
"team-1"
|
|
123
|
+
],
|
|
124
|
+
false,
|
|
125
|
+
false
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
"matches both rules",
|
|
129
|
+
"user-1",
|
|
130
|
+
[
|
|
131
|
+
"team-1"
|
|
132
|
+
],
|
|
133
|
+
false,
|
|
134
|
+
true
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"matches neither rule",
|
|
138
|
+
"user-2",
|
|
139
|
+
[
|
|
140
|
+
"team-2"
|
|
141
|
+
],
|
|
142
|
+
true,
|
|
143
|
+
false
|
|
144
|
+
]
|
|
145
|
+
])("%s", (_, id, teams, visible, disabled)=>{
|
|
146
|
+
const form = createFieldForm(createIdentity(id, teams), rules);
|
|
147
|
+
expect(form.field("secret").visible).toBe(visible);
|
|
148
|
+
expect(form.field("secret").disabled).toBe(disabled);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it("does not match an anonymous identity", ()=>{
|
|
152
|
+
const form = createFieldForm(Identity.createAnonymous(), [
|
|
153
|
+
accessRule("admin:anonymous")
|
|
154
|
+
]);
|
|
155
|
+
expect(form.field("secret").visible).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it.each([
|
|
158
|
+
[
|
|
159
|
+
"no scope",
|
|
160
|
+
"user-1"
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"unknown scope",
|
|
164
|
+
"role:user-1"
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"empty id",
|
|
168
|
+
"admin:"
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
"null",
|
|
172
|
+
null
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
"non-string",
|
|
176
|
+
1
|
|
177
|
+
]
|
|
178
|
+
])("ignores a malformed value (%s)", (_, value)=>{
|
|
179
|
+
const form = createFieldForm(createIdentity("user-1"), [
|
|
180
|
+
accessRule(value)
|
|
181
|
+
]);
|
|
182
|
+
expect(form.field("secret").visible).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
it("hides a tab from the admin the rule names, alongside a condition rule", ()=>{
|
|
185
|
+
const form = createContainer(createIdentity("user-1")).resolve(FormModelFactory).create({
|
|
186
|
+
fields: (fields)=>({
|
|
187
|
+
options: fields.text().label("Options"),
|
|
188
|
+
map: fields.text().label("Map")
|
|
189
|
+
}),
|
|
190
|
+
layout: (layout)=>[
|
|
191
|
+
layout.tabs("tabs").tab("tab1", (tab)=>{
|
|
192
|
+
tab.label("Tab 1").layout((layout)=>[
|
|
193
|
+
layout.row("options")
|
|
194
|
+
]);
|
|
195
|
+
}).tab("tab2", (tab)=>{
|
|
196
|
+
tab.label("Tab 2").rules([
|
|
197
|
+
{
|
|
198
|
+
type: "condition",
|
|
199
|
+
target: "options",
|
|
200
|
+
operator: "!=",
|
|
201
|
+
value: "map",
|
|
202
|
+
action: "disable"
|
|
203
|
+
},
|
|
204
|
+
accessRule("admin:user-1")
|
|
205
|
+
]).layout((layout)=>[
|
|
206
|
+
layout.row("map")
|
|
207
|
+
]);
|
|
208
|
+
})
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
const tabs = form.vm.layout.find((n)=>"tabs" === n.type);
|
|
212
|
+
expect(tabs.tabs.map((t)=>t.id)).toEqual([
|
|
213
|
+
"tab1"
|
|
214
|
+
]);
|
|
215
|
+
expect(form.field("map").visible).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=AccessControlRuleEvaluator.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/formModel/AccessControlRuleEvaluator.test.js","sources":["../../../src/features/formModel/AccessControlRuleEvaluator.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { Container } from \"@webiny/di\";\nimport { FormModelFeature } from \"./feature.js\";\nimport { FormModelFactory, type IRule, type ITabsNodeVM } from \"./abstractions.js\";\nimport { IdentityContext } from \"~/features/security/IdentityContext/abstractions.js\";\nimport { Identity } from \"~/domain/Identity.js\";\n\nconst createIdentity = (id: string, teamSlugs: string[] = []) =>\n Identity.createAuthenticated({\n id,\n type: \"admin\",\n displayName: id,\n roles: [],\n teams: teamSlugs.map(slug => ({ id: `${slug}-id`, slug, name: slug })),\n permissions: [],\n profile: { external: false },\n currentTenant: { id: \"root\", name: \"Root\" },\n defaultTenant: { id: \"root\", name: \"Root\" }\n } as Identity.Data);\n\nconst accessRule = (value: IRule[\"value\"], action: IRule[\"action\"] = \"hide\"): IRule => ({\n type: \"accessControl\",\n target: \"identity\",\n operator: \"matches\",\n value,\n action\n});\n\nconst createContainer = (identity: Identity) => {\n const container = new Container();\n FormModelFeature.register(container);\n container.registerInstance(IdentityContext, {\n getIdentity: () => identity,\n setIdentity: () => {},\n clear: () => {}\n });\n return container;\n};\n\nconst createFieldForm = (identity: Identity, rules: IRule[]) =>\n createContainer(identity)\n .resolve(FormModelFactory)\n .create({\n fields: fields => ({\n secret: fields.text().label(\"Secret\").rules(rules)\n })\n });\n\ndescribe(\"AccessControlRuleEvaluator\", () => {\n it(\"hides a field from the admin the rule names\", () => {\n const form = createFieldForm(createIdentity(\"user-1\"), [accessRule(\"admin:user-1\")]);\n\n expect(form.field(\"secret\").visible).toBe(false);\n });\n\n it(\"leaves the field visible for other admins\", () => {\n const form = createFieldForm(createIdentity(\"user-2\"), [accessRule(\"admin:user-1\")]);\n\n expect(form.field(\"secret\").visible).toBe(true);\n });\n\n it(\"hides a field from members of the team the rule names, by team slug\", () => {\n const form = createFieldForm(createIdentity(\"user-2\", [\"editors\"]), [\n accessRule(\"team:editors\")\n ]);\n\n expect(form.field(\"secret\").visible).toBe(false);\n });\n\n it(\"leaves the field visible for admins outside the team\", () => {\n const form = createFieldForm(createIdentity(\"user-2\", [\"writers\"]), [\n accessRule(\"team:editors\")\n ]);\n\n expect(form.field(\"secret\").visible).toBe(true);\n });\n\n it(\"does not match a team by its id\", () => {\n const form = createFieldForm(createIdentity(\"user-2\", [\"editors\"]), [\n accessRule(\"team:editors-id\")\n ]);\n\n expect(form.field(\"secret\").visible).toBe(true);\n });\n\n it(\"disables the field for a viewer rule\", () => {\n const form = createFieldForm(createIdentity(\"user-1\"), [\n accessRule(\"admin:user-1\", \"disable\")\n ]);\n\n expect(form.field(\"secret\").visible).toBe(true);\n expect(form.field(\"secret\").vm.disabled).toBe(true);\n });\n\n it(\"applies when any of several rules matches\", () => {\n const form = createFieldForm(createIdentity(\"user-3\", [\"editors\"]), [\n accessRule(\"admin:user-1\"),\n accessRule(\"team:editors\")\n ]);\n\n expect(form.field(\"secret\").visible).toBe(false);\n });\n\n describe(\"read-only and no-access rules on one field\", () => {\n const rules = [accessRule(\"admin:user-1\", \"disable\"), accessRule(\"team:team-1\", \"hide\")];\n\n it.each([\n [\"matches only the read-only rule\", \"user-1\", [], true, true],\n [\"matches only the no-access rule\", \"user-2\", [\"team-1\"], false, false],\n [\"matches both rules\", \"user-1\", [\"team-1\"], false, true],\n [\"matches neither rule\", \"user-2\", [\"team-2\"], true, false]\n ])(\"%s\", (_, id, teams, visible, disabled) => {\n const form = createFieldForm(createIdentity(id, teams), rules);\n\n expect(form.field(\"secret\").visible).toBe(visible);\n expect(form.field(\"secret\").disabled).toBe(disabled);\n });\n });\n\n it(\"does not match an anonymous identity\", () => {\n const form = createFieldForm(Identity.createAnonymous(), [accessRule(\"admin:anonymous\")]);\n\n expect(form.field(\"secret\").visible).toBe(true);\n });\n\n it.each([\n [\"no scope\", \"user-1\"],\n [\"unknown scope\", \"role:user-1\"],\n [\"empty id\", \"admin:\"],\n [\"null\", null],\n [\"non-string\", 1]\n ])(\"ignores a malformed value (%s)\", (_, value) => {\n const form = createFieldForm(createIdentity(\"user-1\"), [accessRule(value)]);\n\n expect(form.field(\"secret\").visible).toBe(true);\n });\n\n it(\"hides a tab from the admin the rule names, alongside a condition rule\", () => {\n const form = createContainer(createIdentity(\"user-1\"))\n .resolve(FormModelFactory)\n .create({\n fields: fields => ({\n options: fields.text().label(\"Options\"),\n map: fields.text().label(\"Map\")\n }),\n layout: layout => [\n layout\n .tabs(\"tabs\")\n .tab(\"tab1\", tab => {\n tab.label(\"Tab 1\").layout(layout => [layout.row(\"options\")]);\n })\n .tab(\"tab2\", tab => {\n tab.label(\"Tab 2\")\n .rules([\n {\n type: \"condition\",\n target: \"options\",\n operator: \"!=\",\n value: \"map\",\n action: \"disable\"\n },\n accessRule(\"admin:user-1\")\n ])\n .layout(layout => [layout.row(\"map\")]);\n })\n ]\n });\n\n const tabs = form.vm.layout.find(n => n.type === \"tabs\") as ITabsNodeVM;\n expect(tabs.tabs.map(t => t.id)).toEqual([\"tab1\"]);\n expect(form.field(\"map\").visible).toBe(false);\n });\n});\n"],"names":["createIdentity","id","teamSlugs","Identity","slug","accessRule","value","action","createContainer","identity","container","Container","FormModelFeature","IdentityContext","createFieldForm","rules","FormModelFactory","fields","describe","it","form","expect","_","teams","visible","disabled","layout","tab","tabs","n","t"],"mappings":";;;;;;AAOA,MAAMA,iBAAiB,CAACC,IAAYC,YAAsB,EAAE,GACxDC,SAAS,mBAAmB,CAAC;QACzBF;QACA,MAAM;QACN,aAAaA;QACb,OAAO,EAAE;QACT,OAAOC,UAAU,GAAG,CAACE,CAAAA,OAAS;gBAAE,IAAI,GAAGA,KAAK,GAAG,CAAC;gBAAEA;gBAAM,MAAMA;YAAK;QACnE,aAAa,EAAE;QACf,SAAS;YAAE,UAAU;QAAM;QAC3B,eAAe;YAAE,IAAI;YAAQ,MAAM;QAAO;QAC1C,eAAe;YAAE,IAAI;YAAQ,MAAM;QAAO;IAC9C;AAEJ,MAAMC,aAAa,CAACC,OAAuBC,SAA0B,MAAM,GAAa;QACpF,MAAM;QACN,QAAQ;QACR,UAAU;QACVD;QACAC;IACJ;AAEA,MAAMC,kBAAkB,CAACC;IACrB,MAAMC,YAAY,IAAIC;IACtBC,iBAAiB,QAAQ,CAACF;IAC1BA,UAAU,gBAAgB,CAACG,iBAAiB;QACxC,aAAa,IAAMJ;QACnB,aAAa,KAAO;QACpB,OAAO,KAAO;IAClB;IACA,OAAOC;AACX;AAEA,MAAMI,kBAAkB,CAACL,UAAoBM,QACzCP,gBAAgBC,UACX,OAAO,CAACO,kBACR,MAAM,CAAC;QACJ,QAAQC,CAAAA,SAAW;gBACf,QAAQA,OAAO,IAAI,GAAG,KAAK,CAAC,UAAU,KAAK,CAACF;YAChD;IACJ;AAERG,SAAS,8BAA8B;IACnCC,GAAG,+CAA+C;QAC9C,MAAMC,OAAON,gBAAgBd,eAAe,WAAW;YAACK,WAAW;SAAgB;QAEnFgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAD,GAAG,6CAA6C;QAC5C,MAAMC,OAAON,gBAAgBd,eAAe,WAAW;YAACK,WAAW;SAAgB;QAEnFgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAD,GAAG,uEAAuE;QACtE,MAAMC,OAAON,gBAAgBd,eAAe,UAAU;YAAC;SAAU,GAAG;YAChEK,WAAW;SACd;QAEDgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAD,GAAG,wDAAwD;QACvD,MAAMC,OAAON,gBAAgBd,eAAe,UAAU;YAAC;SAAU,GAAG;YAChEK,WAAW;SACd;QAEDgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAD,GAAG,mCAAmC;QAClC,MAAMC,OAAON,gBAAgBd,eAAe,UAAU;YAAC;SAAU,GAAG;YAChEK,WAAW;SACd;QAEDgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAD,GAAG,wCAAwC;QACvC,MAAMC,OAAON,gBAAgBd,eAAe,WAAW;YACnDK,WAAW,gBAAgB;SAC9B;QAEDgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;QAC1CC,OAAOD,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC;IAClD;IAEAD,GAAG,6CAA6C;QAC5C,MAAMC,OAAON,gBAAgBd,eAAe,UAAU;YAAC;SAAU,GAAG;YAChEK,WAAW;YACXA,WAAW;SACd;QAEDgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAF,SAAS,8CAA8C;QACnD,MAAMH,QAAQ;YAACV,WAAW,gBAAgB;YAAYA,WAAW,eAAe;SAAQ;QAExFc,GAAG,IAAI,CAAC;YACJ;gBAAC;gBAAmC;gBAAU,EAAE;gBAAE;gBAAM;aAAK;YAC7D;gBAAC;gBAAmC;gBAAU;oBAAC;iBAAS;gBAAE;gBAAO;aAAM;YACvE;gBAAC;gBAAsB;gBAAU;oBAAC;iBAAS;gBAAE;gBAAO;aAAK;YACzD;gBAAC;gBAAwB;gBAAU;oBAAC;iBAAS;gBAAE;gBAAM;aAAM;SAC9D,EAAE,MAAM,CAACG,GAAGrB,IAAIsB,OAAOC,SAASC;YAC7B,MAAML,OAAON,gBAAgBd,eAAeC,IAAIsB,QAAQR;YAExDM,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAACI;YAC1CH,OAAOD,KAAK,KAAK,CAAC,UAAU,QAAQ,EAAE,IAAI,CAACK;QAC/C;IACJ;IAEAN,GAAG,wCAAwC;QACvC,MAAMC,OAAON,gBAAgBX,SAAS,eAAe,IAAI;YAACE,WAAW;SAAmB;QAExFgB,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAD,GAAG,IAAI,CAAC;QACJ;YAAC;YAAY;SAAS;QACtB;YAAC;YAAiB;SAAc;QAChC;YAAC;YAAY;SAAS;QACtB;YAAC;YAAQ;SAAK;QACd;YAAC;YAAc;SAAE;KACpB,EAAE,kCAAkC,CAACG,GAAGhB;QACrC,MAAMc,OAAON,gBAAgBd,eAAe,WAAW;YAACK,WAAWC;SAAO;QAE1Ee,OAAOD,KAAK,KAAK,CAAC,UAAU,OAAO,EAAE,IAAI,CAAC;IAC9C;IAEAD,GAAG,yEAAyE;QACxE,MAAMC,OAAOZ,gBAAgBR,eAAe,WACvC,OAAO,CAACgB,kBACR,MAAM,CAAC;YACJ,QAAQC,CAAAA,SAAW;oBACf,SAASA,OAAO,IAAI,GAAG,KAAK,CAAC;oBAC7B,KAAKA,OAAO,IAAI,GAAG,KAAK,CAAC;gBAC7B;YACA,QAAQS,CAAAA,SAAU;oBACdA,OACK,IAAI,CAAC,QACL,GAAG,CAAC,QAAQC,CAAAA;wBACTA,IAAI,KAAK,CAAC,SAAS,MAAM,CAACD,CAAAA,SAAU;gCAACA,OAAO,GAAG,CAAC;6BAAW;oBAC/D,GACC,GAAG,CAAC,QAAQC,CAAAA;wBACTA,IAAI,KAAK,CAAC,SACL,KAAK,CAAC;4BACH;gCACI,MAAM;gCACN,QAAQ;gCACR,UAAU;gCACV,OAAO;gCACP,QAAQ;4BACZ;4BACAtB,WAAW;yBACd,EACA,MAAM,CAACqB,CAAAA,SAAU;gCAACA,OAAO,GAAG,CAAC;6BAAO;oBAC7C;iBACP;QACL;QAEJ,MAAME,OAAOR,KAAK,EAAE,CAAC,MAAM,CAAC,IAAI,CAACS,CAAAA,IAAKA,AAAW,WAAXA,EAAE,IAAI;QAC5CR,OAAOO,KAAK,IAAI,CAAC,GAAG,CAACE,CAAAA,IAAKA,EAAE,EAAE,GAAG,OAAO,CAAC;YAAC;SAAO;QACjDT,OAAOD,KAAK,KAAK,CAAC,OAAO,OAAO,EAAE,IAAI,CAAC;IAC3C;AACJ"}
|
|
@@ -4,6 +4,10 @@ const isEmpty = (value)=>{
|
|
|
4
4
|
if (Array.isArray(value) && 0 === value.length) return true;
|
|
5
5
|
return false;
|
|
6
6
|
};
|
|
7
|
+
const isEqual = (value, ruleValue)=>{
|
|
8
|
+
if ("boolean" == typeof value || "boolean" == typeof ruleValue) return value === ruleValue;
|
|
9
|
+
return value == ruleValue;
|
|
10
|
+
};
|
|
7
11
|
class ConditionRuleEvaluatorImpl {
|
|
8
12
|
canEvaluate(rule) {
|
|
9
13
|
return "condition" === rule.type;
|
|
@@ -12,14 +16,28 @@ class ConditionRuleEvaluatorImpl {
|
|
|
12
16
|
const field = safeGetField(form, rule.target);
|
|
13
17
|
const value = field?.getValue() ?? null;
|
|
14
18
|
switch(rule.operator){
|
|
19
|
+
case "==":
|
|
20
|
+
case "eq":
|
|
21
|
+
return isEqual(value, rule.value);
|
|
22
|
+
case "!=":
|
|
23
|
+
case "neq":
|
|
24
|
+
return !isEqual(value, rule.value);
|
|
25
|
+
case ">":
|
|
26
|
+
case "gt":
|
|
27
|
+
return Number(value) > Number(rule.value);
|
|
28
|
+
case "<":
|
|
29
|
+
case "lt":
|
|
30
|
+
return Number(value) < Number(rule.value);
|
|
31
|
+
case ">=":
|
|
32
|
+
case "gte":
|
|
33
|
+
return Number(value) >= Number(rule.value);
|
|
34
|
+
case "<=":
|
|
35
|
+
case "lte":
|
|
36
|
+
return Number(value) <= Number(rule.value);
|
|
15
37
|
case "isEmpty":
|
|
16
38
|
return isEmpty(value);
|
|
17
39
|
case "isNotEmpty":
|
|
18
40
|
return !isEmpty(value);
|
|
19
|
-
case "eq":
|
|
20
|
-
return value === rule.value;
|
|
21
|
-
case "neq":
|
|
22
|
-
return value !== rule.value;
|
|
23
41
|
case "isTruthy":
|
|
24
42
|
return !!value;
|
|
25
43
|
case "isFalsy":
|
|
@@ -27,6 +45,18 @@ class ConditionRuleEvaluatorImpl {
|
|
|
27
45
|
case "matches":
|
|
28
46
|
if ("string" != typeof value || null === rule.value) return false;
|
|
29
47
|
return value === rule.value;
|
|
48
|
+
case "contains":
|
|
49
|
+
return String(value ?? "").includes(String(rule.value ?? ""));
|
|
50
|
+
case "notContains":
|
|
51
|
+
return !String(value ?? "").includes(String(rule.value ?? ""));
|
|
52
|
+
case "startsWith":
|
|
53
|
+
return String(value ?? "").startsWith(String(rule.value ?? ""));
|
|
54
|
+
case "notStartsWith":
|
|
55
|
+
return !String(value ?? "").startsWith(String(rule.value ?? ""));
|
|
56
|
+
case "endsWith":
|
|
57
|
+
return String(value ?? "").endsWith(String(rule.value ?? ""));
|
|
58
|
+
case "notEndsWith":
|
|
59
|
+
return !String(value ?? "").endsWith(String(rule.value ?? ""));
|
|
30
60
|
default:
|
|
31
61
|
if ("development" === process.env.NODE_ENV) console.warn(`[FormModel] Unknown operator "${rule.operator}" in condition rule. Returning false.`);
|
|
32
62
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features/formModel/ConditionRuleEvaluator.js","sources":["../../../src/features/formModel/ConditionRuleEvaluator.ts"],"sourcesContent":["import type { IFormModel } from \"./abstractions.js\";\nimport { RuleEvaluator } from \"./abstractions.js\";\n\nconst isEmpty = (value: unknown): boolean => {\n if (value === null || value === undefined || value === \"\") {\n return true;\n }\n if (Array.isArray(value) && value.length === 0) {\n return true;\n }\n return false;\n};\n\nexport class ConditionRuleEvaluatorImpl implements RuleEvaluator.Interface {\n canEvaluate(rule: RuleEvaluator.Rule): boolean {\n return rule.type === \"condition\";\n }\n\n evaluate(rule: RuleEvaluator.Rule, form: IFormModel): boolean {\n const field = safeGetField(form, rule.target);\n const value = field?.getValue() ?? null;\n\n switch (rule.operator) {\n case \"
|
|
1
|
+
{"version":3,"file":"features/formModel/ConditionRuleEvaluator.js","sources":["../../../src/features/formModel/ConditionRuleEvaluator.ts"],"sourcesContent":["import type { IFormModel } from \"./abstractions.js\";\nimport { RuleEvaluator } from \"./abstractions.js\";\n\nconst isEmpty = (value: unknown): boolean => {\n if (value === null || value === undefined || value === \"\") {\n return true;\n }\n if (Array.isArray(value) && value.length === 0) {\n return true;\n }\n return false;\n};\n\n// Loose equality coerces booleans to numbers (\"\" == false, 0 == false), so compare them strictly.\nconst isEqual = (value: unknown, ruleValue: unknown): boolean => {\n if (typeof value === \"boolean\" || typeof ruleValue === \"boolean\") {\n return value === ruleValue;\n }\n return value == ruleValue;\n};\n\nexport class ConditionRuleEvaluatorImpl implements RuleEvaluator.Interface {\n canEvaluate(rule: RuleEvaluator.Rule): boolean {\n return rule.type === \"condition\";\n }\n\n evaluate(rule: RuleEvaluator.Rule, form: IFormModel): boolean {\n const field = safeGetField(form, rule.target);\n const value = field?.getValue() ?? null;\n\n switch (rule.operator) {\n case \"==\":\n case \"eq\":\n return isEqual(value, rule.value);\n case \"!=\":\n case \"neq\":\n return !isEqual(value, rule.value);\n case \">\":\n case \"gt\":\n return Number(value) > Number(rule.value);\n case \"<\":\n case \"lt\":\n return Number(value) < Number(rule.value);\n case \">=\":\n case \"gte\":\n return Number(value) >= Number(rule.value);\n case \"<=\":\n case \"lte\":\n return Number(value) <= Number(rule.value);\n case \"isEmpty\":\n return isEmpty(value);\n case \"isNotEmpty\":\n return !isEmpty(value);\n case \"isTruthy\":\n return !!value;\n case \"isFalsy\":\n return !value;\n case \"matches\":\n if (typeof value !== \"string\" || rule.value === null) {\n return false;\n }\n return value === rule.value;\n case \"contains\":\n return String(value ?? \"\").includes(String(rule.value ?? \"\"));\n case \"notContains\":\n return !String(value ?? \"\").includes(String(rule.value ?? \"\"));\n case \"startsWith\":\n return String(value ?? \"\").startsWith(String(rule.value ?? \"\"));\n case \"notStartsWith\":\n return !String(value ?? \"\").startsWith(String(rule.value ?? \"\"));\n case \"endsWith\":\n return String(value ?? \"\").endsWith(String(rule.value ?? \"\"));\n case \"notEndsWith\":\n return !String(value ?? \"\").endsWith(String(rule.value ?? \"\"));\n default:\n if (process.env.NODE_ENV === \"development\") {\n console.warn(\n `[FormModel] Unknown operator \"${rule.operator}\" in condition rule. Returning false.`\n );\n }\n return false;\n }\n }\n}\n\nfunction safeGetField(form: IFormModel, name: string) {\n try {\n return form.field(name);\n } catch {\n return undefined;\n }\n}\n\nexport const ConditionRuleEvaluator = RuleEvaluator.createImplementation({\n implementation: ConditionRuleEvaluatorImpl,\n dependencies: []\n});\n"],"names":["isEmpty","value","Array","isEqual","ruleValue","ConditionRuleEvaluatorImpl","rule","form","field","safeGetField","Number","String","process","console","name","ConditionRuleEvaluator","RuleEvaluator"],"mappings":";AAGA,MAAMA,UAAU,CAACC;IACb,IAAIA,QAAAA,SAAyCA,AAAU,OAAVA,OACzC,OAAO;IAEX,IAAIC,MAAM,OAAO,CAACD,UAAUA,AAAiB,MAAjBA,MAAM,MAAM,EACpC,OAAO;IAEX,OAAO;AACX;AAGA,MAAME,UAAU,CAACF,OAAgBG;IAC7B,IAAI,AAAiB,aAAjB,OAAOH,SAAuB,AAAqB,aAArB,OAAOG,WACrC,OAAOH,UAAUG;IAErB,OAAOH,SAASG;AACpB;AAEO,MAAMC;IACT,YAAYC,IAAwB,EAAW;QAC3C,OAAOA,AAAc,gBAAdA,KAAK,IAAI;IACpB;IAEA,SAASA,IAAwB,EAAEC,IAAgB,EAAW;QAC1D,MAAMC,QAAQC,aAAaF,MAAMD,KAAK,MAAM;QAC5C,MAAML,QAAQO,OAAO,cAAc;QAEnC,OAAQF,KAAK,QAAQ;YACjB,KAAK;YACL,KAAK;gBACD,OAAOH,QAAQF,OAAOK,KAAK,KAAK;YACpC,KAAK;YACL,KAAK;gBACD,OAAO,CAACH,QAAQF,OAAOK,KAAK,KAAK;YACrC,KAAK;YACL,KAAK;gBACD,OAAOI,OAAOT,SAASS,OAAOJ,KAAK,KAAK;YAC5C,KAAK;YACL,KAAK;gBACD,OAAOI,OAAOT,SAASS,OAAOJ,KAAK,KAAK;YAC5C,KAAK;YACL,KAAK;gBACD,OAAOI,OAAOT,UAAUS,OAAOJ,KAAK,KAAK;YAC7C,KAAK;YACL,KAAK;gBACD,OAAOI,OAAOT,UAAUS,OAAOJ,KAAK,KAAK;YAC7C,KAAK;gBACD,OAAON,QAAQC;YACnB,KAAK;gBACD,OAAO,CAACD,QAAQC;YACpB,KAAK;gBACD,OAAO,CAAC,CAACA;YACb,KAAK;gBACD,OAAO,CAACA;YACZ,KAAK;gBACD,IAAI,AAAiB,YAAjB,OAAOA,SAAsBK,AAAe,SAAfA,KAAK,KAAK,EACvC,OAAO;gBAEX,OAAOL,UAAUK,KAAK,KAAK;YAC/B,KAAK;gBACD,OAAOK,OAAOV,SAAS,IAAI,QAAQ,CAACU,OAAOL,KAAK,KAAK,IAAI;YAC7D,KAAK;gBACD,OAAO,CAACK,OAAOV,SAAS,IAAI,QAAQ,CAACU,OAAOL,KAAK,KAAK,IAAI;YAC9D,KAAK;gBACD,OAAOK,OAAOV,SAAS,IAAI,UAAU,CAACU,OAAOL,KAAK,KAAK,IAAI;YAC/D,KAAK;gBACD,OAAO,CAACK,OAAOV,SAAS,IAAI,UAAU,CAACU,OAAOL,KAAK,KAAK,IAAI;YAChE,KAAK;gBACD,OAAOK,OAAOV,SAAS,IAAI,QAAQ,CAACU,OAAOL,KAAK,KAAK,IAAI;YAC7D,KAAK;gBACD,OAAO,CAACK,OAAOV,SAAS,IAAI,QAAQ,CAACU,OAAOL,KAAK,KAAK,IAAI;YAC9D;gBACI,IAAIM,AAAyB,kBAAzBA,QAAQ,GAAG,CAAC,QAAQ,EACpBC,QAAQ,IAAI,CACR,CAAC,8BAA8B,EAAEP,KAAK,QAAQ,CAAC,qCAAqC,CAAC;gBAG7F,OAAO;QACf;IACJ;AACJ;AAEA,SAASG,aAAaF,IAAgB,EAAEO,IAAY;IAChD,IAAI;QACA,OAAOP,KAAK,KAAK,CAACO;IACtB,EAAE,OAAM;QACJ;IACJ;AACJ;AAEO,MAAMC,yBAAyBC,cAAc,oBAAoB,CAAC;IACrE,gBAAgBX;IAChB,cAAc,EAAE;AACpB"}
|
|
@@ -52,6 +52,7 @@ export declare class Field implements IField {
|
|
|
52
52
|
setVisible(value: boolean): void;
|
|
53
53
|
setAncestorRules(rules: IRule[]): void;
|
|
54
54
|
setValidation(validation: IFieldValidation): void;
|
|
55
|
+
setValidationWithCache(validation: IFieldValidation, value: unknown, result: boolean): void;
|
|
55
56
|
resetValidation(): void;
|
|
56
57
|
addBeforeChange(cb: BeforeChangeCallback): void;
|
|
57
58
|
addAfterChange(cb: AfterChangeCallback): void;
|
|
@@ -184,6 +184,11 @@ class Field {
|
|
|
184
184
|
setValidation(validation) {
|
|
185
185
|
this._validation = validation;
|
|
186
186
|
}
|
|
187
|
+
setValidationWithCache(validation, value, result) {
|
|
188
|
+
this._validation = validation;
|
|
189
|
+
this._validationCacheKey = this._serializeValue(value);
|
|
190
|
+
this._validationCache = result;
|
|
191
|
+
}
|
|
187
192
|
resetValidation() {
|
|
188
193
|
this._validation = {
|
|
189
194
|
isValid: null
|
|
@@ -376,6 +381,17 @@ class Field {
|
|
|
376
381
|
return false;
|
|
377
382
|
}
|
|
378
383
|
}
|
|
384
|
+
if (!requiredState.required && null == effectiveValue) {
|
|
385
|
+
const cacheKey = this._serializeValue(effectiveValue);
|
|
386
|
+
runInAction(()=>{
|
|
387
|
+
this._validation = {
|
|
388
|
+
isValid: null
|
|
389
|
+
};
|
|
390
|
+
this._validationCacheKey = cacheKey;
|
|
391
|
+
this._validationCache = true;
|
|
392
|
+
});
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
379
395
|
if (this.config.schema) {
|
|
380
396
|
const result = await this.config.schema.safeParseAsync(effectiveValue);
|
|
381
397
|
if (!result.success) {
|