@webiny/app-admin 6.6.0-alpha.8 → 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.
Files changed (31) hide show
  1. package/base/Base/FieldRenderers/AutoCompleteRenderer.js +1 -0
  2. package/base/Base/FieldRenderers/AutoCompleteRenderer.js.map +1 -1
  3. package/base/Base/FieldRenderers/RadioButtonsRenderer.js +3 -2
  4. package/base/Base/FieldRenderers/RadioButtonsRenderer.js.map +1 -1
  5. package/base/Base/FieldRenderers/SelectRenderer.js +4 -2
  6. package/base/Base/FieldRenderers/SelectRenderer.js.map +1 -1
  7. package/features/formModel/AccessControlRuleEvaluator.d.ts +12 -0
  8. package/features/formModel/AccessControlRuleEvaluator.js +30 -0
  9. package/features/formModel/AccessControlRuleEvaluator.js.map +1 -0
  10. package/features/formModel/AccessControlRuleEvaluator.test.d.ts +1 -0
  11. package/features/formModel/AccessControlRuleEvaluator.test.js +219 -0
  12. package/features/formModel/AccessControlRuleEvaluator.test.js.map +1 -0
  13. package/features/formModel/ConditionRuleEvaluator.js +34 -4
  14. package/features/formModel/ConditionRuleEvaluator.js.map +1 -1
  15. package/features/formModel/Field.d.ts +1 -0
  16. package/features/formModel/Field.js +16 -0
  17. package/features/formModel/Field.js.map +1 -1
  18. package/features/formModel/FieldBuilder.test.js +2 -0
  19. package/features/formModel/FieldBuilder.test.js.map +1 -1
  20. package/features/formModel/FormModel.test.js +248 -2
  21. package/features/formModel/FormModel.test.js.map +1 -1
  22. package/features/formModel/ObjectField.d.ts +1 -0
  23. package/features/formModel/ObjectField.js +24 -0
  24. package/features/formModel/ObjectField.js.map +1 -1
  25. package/features/formModel/Rules.test.js +169 -16
  26. package/features/formModel/Rules.test.js.map +1 -1
  27. package/features/formModel/abstractions.d.ts +2 -2
  28. package/features/formModel/abstractions.js.map +1 -1
  29. package/features/formModel/feature.js +2 -0
  30. package/features/formModel/feature.js.map +1 -1
  31. package/package.json +20 -20
@@ -15,6 +15,7 @@ const AutoCompleteRenderer = createFieldRenderer(({ field })=>{
15
15
  field.onBlur();
16
16
  },
17
17
  required: field.required,
18
+ displayResetAction: !field.required,
18
19
  disabled: field.disabled,
19
20
  validation: field.validation,
20
21
  options: options.map((opt)=>({
@@ -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;QACxB,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
+ {"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: void 0 !== field.value ? String(field.value) : void 0,
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 !== undefined ? String(field.value) : undefined}\n onChange={value => {\n field.onChange(value);\n field.onBlur();\n }}\n />\n );\n});\n"],"names":["RadioButtonsRenderer","createFieldRenderer","field","options","RadioGroup","opt","String","undefined","value"],"mappings":";;;AAWO,MAAMA,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,OAAOC,OAAOD,IAAI,KAAK;YAC3B;QACA,OAAOH,AAAgBK,WAAhBL,MAAM,KAAK,GAAiBI,OAAOJ,MAAM,KAAK,IAAIK;QACzD,UAAUC,CAAAA;YACNN,MAAM,QAAQ,CAACM;YACfN,MAAM,MAAM;QAChB;;AAGZ"}
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: null != field.value ? String(field.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 != null ? String(field.value) : \"\"}\n onChange={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":["SelectRenderer","createFieldRenderer","field","options","Select","String","value","opt"],"mappings":";;;AAaO,MAAMA,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,OAAOA,AAAe,QAAfA,MAAM,KAAK,GAAWG,OAAOH,MAAM,KAAK,IAAI;QACnD,UAAUI,CAAAA;YACNJ,MAAM,QAAQ,CAACI;YACfJ,MAAM,MAAM;QAChB;QACA,UAAUA,MAAM,QAAQ;QACxB,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
+ {"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,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":"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"}
@@ -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 \"isEmpty\":\n return isEmpty(value);\n case \"isNotEmpty\":\n return !isEmpty(value);\n case \"eq\":\n return value === rule.value;\n case \"neq\":\n return value !== rule.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 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","ConditionRuleEvaluatorImpl","rule","form","field","safeGetField","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;AAEO,MAAME;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,MAAMH,QAAQK,OAAO,cAAc;QAEnC,OAAQF,KAAK,QAAQ;YACjB,KAAK;gBACD,OAAOJ,QAAQC;YACnB,KAAK;gBACD,OAAO,CAACD,QAAQC;YACpB,KAAK;gBACD,OAAOA,UAAUG,KAAK,KAAK;YAC/B,KAAK;gBACD,OAAOH,UAAUG,KAAK,KAAK;YAC/B,KAAK;gBACD,OAAO,CAAC,CAACH;YACb,KAAK;gBACD,OAAO,CAACA;YACZ,KAAK;gBACD,IAAI,AAAiB,YAAjB,OAAOA,SAAsBG,AAAe,SAAfA,KAAK,KAAK,EACvC,OAAO;gBAEX,OAAOH,UAAUG,KAAK,KAAK;YAC/B;gBACI,IAAII,AAAyB,kBAAzBA,QAAQ,GAAG,CAAC,QAAQ,EACpBC,QAAQ,IAAI,CACR,CAAC,8BAA8B,EAAEL,KAAK,QAAQ,CAAC,qCAAqC,CAAC;gBAG7F,OAAO;QACf;IACJ;AACJ;AAEA,SAASG,aAAaF,IAAgB,EAAEK,IAAY;IAChD,IAAI;QACA,OAAOL,KAAK,KAAK,CAACK;IACtB,EAAE,OAAM;QACJ;IACJ;AACJ;AAEO,MAAMC,yBAAyBC,cAAc,oBAAoB,CAAC;IACrE,gBAAgBT;IAChB,cAAc,EAAE;AACpB"}
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) {