@projectcaluma/ember-form 10.0.2 → 11.0.0-beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. package/CHANGELOG.md +1112 -0
  2. package/addon/components/cf-content.hbs +36 -39
  3. package/addon/components/cf-content.js +48 -29
  4. package/addon/components/cf-field/info.hbs +1 -1
  5. package/addon/components/cf-field/input/action-button.hbs +1 -1
  6. package/addon/components/cf-field/input/action-button.js +9 -7
  7. package/addon/components/cf-field/input/checkbox.hbs +6 -2
  8. package/addon/components/cf-field/input/checkbox.js +9 -29
  9. package/addon/components/cf-field/input/file.hbs +1 -1
  10. package/addon/components/cf-field/input/file.js +8 -9
  11. package/addon/components/cf-field/input/float.hbs +4 -4
  12. package/addon/components/cf-field/input/integer.hbs +5 -5
  13. package/addon/components/cf-field/input/radio.hbs +4 -1
  14. package/addon/components/cf-field/input/table.hbs +7 -7
  15. package/addon/components/cf-field/input/table.js +12 -10
  16. package/addon/components/cf-field/input/text.hbs +5 -5
  17. package/addon/components/cf-field/input/textarea.hbs +6 -5
  18. package/addon/components/cf-field/input.hbs +10 -1
  19. package/addon/components/cf-field/input.js +1 -1
  20. package/addon/components/cf-field/label.hbs +1 -1
  21. package/addon/components/cf-field-value.hbs +1 -1
  22. package/addon/components/cf-field-value.js +8 -13
  23. package/addon/components/cf-field.hbs +2 -2
  24. package/addon/components/cf-field.js +2 -3
  25. package/addon/components/cf-navigation-item.hbs +2 -2
  26. package/addon/components/cf-navigation.hbs +4 -1
  27. package/addon/components/document-validity.js +1 -1
  28. package/addon/gql/fragments/field.graphql +22 -0
  29. package/addon/gql/mutations/save-document-table-answer.graphql +1 -1
  30. package/addon/gql/mutations/save-document.graphql +1 -0
  31. package/addon/gql/queries/{get-document-answers.graphql → document-answers.graphql} +2 -1
  32. package/addon/gql/queries/{get-document-forms.graphql → document-forms.graphql} +2 -1
  33. package/addon/gql/queries/{get-document-used-dynamic-options.graphql → document-used-dynamic-options.graphql} +2 -1
  34. package/addon/gql/queries/{get-dynamic-options.graphql → dynamic-options.graphql} +2 -1
  35. package/addon/gql/queries/{get-fileanswer-info.graphql → fileanswer-info.graphql} +2 -1
  36. package/addon/helpers/get-widget.js +50 -0
  37. package/addon/lib/answer.js +108 -72
  38. package/addon/lib/base.js +32 -23
  39. package/addon/lib/dependencies.js +36 -71
  40. package/addon/lib/document.js +92 -96
  41. package/addon/lib/field.js +334 -401
  42. package/addon/lib/fieldset.js +46 -47
  43. package/addon/lib/form.js +27 -15
  44. package/addon/lib/navigation.js +211 -192
  45. package/addon/lib/question.js +103 -94
  46. package/addon/services/caluma-store.js +10 -6
  47. package/app/helpers/get-widget.js +4 -0
  48. package/blueprints/@projectcaluma/ember-form/index.js +1 -0
  49. package/package.json +27 -23
  50. package/addon/components/cf-navigation.js +0 -9
package/addon/lib/base.js CHANGED
@@ -1,35 +1,44 @@
1
- import { getOwner } from "@ember/application";
1
+ import { setOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import EmberObject from "@ember/object";
3
+ import { registerDestructor } from "@ember/destroyable";
4
4
  import { inject as service } from "@ember/service";
5
5
 
6
- export default EmberObject.extend({
7
- calumaStore: service(),
6
+ export default class Base {
7
+ @service calumaStore;
8
8
 
9
- init(...args) {
10
- this._super(...args);
9
+ constructor({ raw, owner }) {
10
+ assert("`owner` must be passed as an argument", owner);
11
11
 
12
- assert("Owner must be injected", getOwner(this));
12
+ assert("A primary key `pk` must be defined on the object", "pk" in this);
13
13
 
14
- // answers don't need a pk if they are new
15
- if (!/Answer$/.test(this.get("raw.__typename"))) {
16
- assert("A primary key `pk` must be passed", this.pk);
17
- assert(
18
- "The primary key `pk` must be readonly",
19
- !Object.getOwnPropertyDescriptor(this, "pk").writable
20
- );
21
- }
14
+ setOwner(this, owner);
22
15
 
23
- if (this.pk) {
24
- this.calumaStore.push(this);
16
+ if (raw) {
17
+ this.raw = raw;
25
18
  }
26
- },
27
19
 
28
- willDestroy(...args) {
29
- this._super(...args);
20
+ registerDestructor(this, () => {
21
+ if (this.pk) {
22
+ this.calumaStore.delete(this.pk);
23
+ }
24
+ });
25
+ }
30
26
 
27
+ /**
28
+ * The raw data of the object
29
+ *
30
+ * @property {Object} raw
31
+ */
32
+ raw = {};
33
+
34
+ /**
35
+ * Push the object into the caluma store
36
+ *
37
+ * @method pushIntoStore
38
+ */
39
+ pushIntoStore() {
31
40
  if (this.pk) {
32
- this.calumaStore.delete(this.pk);
41
+ this.calumaStore.push(this);
33
42
  }
34
- },
35
- });
43
+ }
44
+ }
@@ -1,4 +1,5 @@
1
- import { computed, get } from "@ember/object";
1
+ import { get } from "@ember/object";
2
+ import { cached } from "tracked-toolbox";
2
3
 
3
4
  import { getAST, getTransforms } from "@projectcaluma/ember-core/utils/jexl";
4
5
 
@@ -47,79 +48,43 @@ export function getDependenciesFromJexl(jexl, expression) {
47
48
  }
48
49
 
49
50
  /**
50
- * Computed property to get all nested dependency parents of an expression. A
51
- * nested dependency parent would be a table field that is used with a mapby
52
- * transform in the JEXL expression.
53
- *
54
- * E.g: 'foo'|answer in 'bar'|answer|mapby('column') where 'bar' would be a
55
- * nested dependency parent.
56
- *
57
- * Those need to be extracted seperately since the overall dependencies need to
58
- * depend on the values of the nested dependency parents to recompute
59
- * correctly.
51
+ * Getter to extract all fields used in an expression.
60
52
  *
61
53
  * @param {String} expressionPath The path of the expression
62
- * @return {Field[]} Returns an array of nested dependency parent fields
54
+ * @return {Field[]} An array of all dependency fields
63
55
  */
64
- export function nestedDependencyParents(expressionPath) {
65
- return dependencies(expressionPath, { onlyNestedParents: true });
66
- }
67
-
68
- /**
69
- * Computed property to get all dependencies of an expression.
70
- *
71
- * @param {String} expressionPath The path of the expression
72
- * @param {Object} options
73
- * @param {Boolean} options.onlyNestedParents Only include nested parent fields
74
- * @param {String} options.nestedParentsPath Path of the nested parent fields to trigger recomputation
75
- * @return {Field[]} Returns an array of all dependency fields
76
- */
77
- export function dependencies(
78
- expressionPath,
79
- { onlyNestedParents = false, nestedParentsPath = null } = {}
80
- ) {
81
- // If there are nested parents we need to recompute the property if their
82
- // values change
83
- const nestedTriggers = nestedParentsPath
84
- ? [`${nestedParentsPath}.@each.value`]
85
- : [];
86
-
87
- return computed(
88
- "document.{jexl,fields.[]}",
89
- expressionPath,
90
- ...nestedTriggers,
91
- function () {
92
- const expression = get(this, expressionPath);
93
-
94
- if (!expression) return [];
95
-
96
- const slugs = getDependenciesFromJexl(this.document.jexl, expression);
97
-
98
- return slugs
99
- .flatMap((slug) => {
100
- const [fieldSlug, nestedSlug = null] = slug.split(".");
101
-
102
- if (onlyNestedParents && !nestedSlug) {
103
- return null;
104
- }
105
-
106
- const field = this.document.findField(fieldSlug);
107
-
108
- if (!onlyNestedParents && nestedSlug && field?.value) {
109
- // Get the nested fields from the parents value (rows)
110
- const childFields =
111
- nestedSlug === "__all__"
112
- ? field.value.flatMap((row) => row.fields)
113
- : field.value.map((row) => row.findField(nestedSlug));
114
-
115
- return [field, ...childFields];
116
- }
117
-
118
- return [field];
119
- })
120
- .filter(Boolean);
121
- }
122
- );
56
+ export function dependencies(expressionPath) {
57
+ return function (target, key) {
58
+ return cached(target, key, {
59
+ get() {
60
+ const expression = get(this, expressionPath);
61
+
62
+ if (!expression) return [];
63
+
64
+ const slugs = getDependenciesFromJexl(this.document.jexl, expression);
65
+
66
+ return slugs
67
+ .flatMap((slug) => {
68
+ const [fieldSlug, nestedSlug = null] = slug.split(".");
69
+
70
+ const field = this.document.findField(fieldSlug);
71
+
72
+ if (nestedSlug && field?.value) {
73
+ // Get the nested fields from the parents value (rows)
74
+ const childFields =
75
+ nestedSlug === "__all__"
76
+ ? field.value.flatMap((row) => row.fields)
77
+ : field.value.map((row) => row.findField(nestedSlug));
78
+
79
+ return [field, ...childFields];
80
+ }
81
+
82
+ return [field];
83
+ })
84
+ .filter(Boolean);
85
+ },
86
+ });
87
+ };
123
88
  }
124
89
 
125
90
  export default dependencies;
@@ -1,8 +1,8 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import { computed, defineProperty } from "@ember/object";
4
- import { inject as service } from "@ember/service";
3
+ import { associateDestroyableChild } from "@ember/destroyable";
5
4
  import jexl from "jexl";
5
+ import { cached } from "tracked-toolbox";
6
6
 
7
7
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
8
8
  import { intersects, mapby } from "@projectcaluma/ember-core/utils/jexl";
@@ -17,124 +17,123 @@ const sum = (nums) => nums.reduce((num, base) => base + num, 0);
17
17
  *
18
18
  * @class Document
19
19
  */
20
- export default Base.extend({
21
- calumaStore: service(),
22
-
23
- init(...args) {
20
+ export default class Document extends Base {
21
+ constructor({ raw, parentDocument, ...args }) {
24
22
  assert(
25
23
  "A graphql document `raw` must be passed",
26
- this.raw && this.raw.__typename === "Document"
24
+ raw?.__typename === "Document"
27
25
  );
28
26
 
29
- defineProperty(this, "pk", {
30
- writable: false,
31
- value: `Document:${decodeId(this.raw.id)}`,
32
- });
27
+ super({ raw, ...args });
33
28
 
34
- this._super(...args);
29
+ this.parentDocument = parentDocument;
35
30
 
36
- this.set("fieldsets", []);
31
+ this.pushIntoStore();
37
32
 
38
33
  this._createRootForm();
39
34
  this._createFieldsets();
40
- },
35
+ }
41
36
 
42
37
  _createRootForm() {
43
- const rootForm =
44
- this.calumaStore.find(`Form:${this.raw.rootForm.slug}`) ||
45
- getOwner(this)
46
- .factoryFor("caluma-model:form")
47
- .create({ raw: this.raw.rootForm });
38
+ const owner = getOwner(this);
48
39
 
49
- this.set("rootForm", rootForm);
50
- },
40
+ this.rootForm =
41
+ this.calumaStore.find(`Form:${this.raw.rootForm.slug}`) ||
42
+ new (owner.factoryFor("caluma-model:form").class)({
43
+ raw: this.raw.rootForm,
44
+ owner,
45
+ });
46
+ }
51
47
 
52
48
  _createFieldsets() {
53
- const fieldsets = this.raw.forms.map((form) => {
54
- return (
49
+ const owner = getOwner(this);
50
+
51
+ this.fieldsets = this.raw.forms.map((form) => {
52
+ return associateDestroyableChild(
53
+ this,
55
54
  this.calumaStore.find(`${this.pk}:Form:${form.slug}`) ||
56
- getOwner(this)
57
- .factoryFor("caluma-model:fieldset")
58
- .create({
55
+ new (owner.factoryFor("caluma-model:fieldset").class)({
59
56
  raw: { form, answers: this.raw.answers },
60
57
  document: this,
58
+ owner,
61
59
  })
62
60
  );
63
61
  });
64
-
65
- this.set("fieldsets", fieldsets);
66
- },
67
-
68
- willDestroy(...args) {
69
- this._super(...args);
70
-
71
- const fieldsets = this.fieldsets;
72
- this.set("fieldsets", []);
73
- fieldsets.forEach((fieldset) => fieldset.destroy());
74
- },
62
+ }
75
63
 
76
64
  /**
77
- * The uuid of the document
65
+ * The parent document of this document. If this is set, the document is most
66
+ * likely a table row.
78
67
  *
79
- * @property {String} uuid
80
- * @accessor
68
+ * @property {Document} parentDocument
81
69
  */
82
- uuid: computed("raw.id", function () {
83
- return decodeId(this.raw.id);
84
- }),
85
-
86
- workItemUuid: computed(
87
- "raw.{workItem.id,case.workItems.edges.[]}",
88
- function () {
89
- // The document is either directly attached to a work item (via
90
- // CompleteTaskFormTask) or it's the case document and therefore
91
- // indirectly attached to a work item (via CompleteWorkflowFormTask)
92
- const rawId =
93
- this.raw.workItem?.id ||
94
- this.raw.case?.workItems.edges.find(
95
- (edge) => edge.node.task.__typename === "CompleteWorkflowFormTask"
96
- )?.node.id;
97
-
98
- return rawId ? decodeId(rawId) : null;
99
- }
100
- ),
70
+ parentDocument = null;
101
71
 
102
72
  /**
103
73
  * The root form of this document
104
74
  *
105
75
  * @property {Form} rootForm
106
- * @accessor
107
76
  */
108
- rootForm: null,
77
+ rootForm = null;
109
78
 
110
79
  /**
111
80
  * The fieldsets of this document
112
81
  *
113
82
  * @property {Fieldset[]} fieldsets
114
- * @accessor
115
83
  */
116
- fieldsets: null,
84
+ fieldsets = [];
85
+
86
+ /**
87
+ * The primary key of the document.
88
+ *
89
+ * @property {String} pk
90
+ */
91
+ @cached
92
+ get pk() {
93
+ return `Document:${this.uuid}`;
94
+ }
95
+
96
+ /**
97
+ * The uuid of the document
98
+ *
99
+ * @property {String} uuid
100
+ */
101
+ @cached
102
+ get uuid() {
103
+ return decodeId(this.raw.id);
104
+ }
105
+
106
+ @cached
107
+ get workItemUuid() {
108
+ // The document is either directly attached to a work item (via
109
+ // CompleteTaskFormTask) or it's the case document and therefore
110
+ // indirectly attached to a work item (via CompleteWorkflowFormTask)
111
+ const rawId =
112
+ this.raw.workItem?.id ||
113
+ this.raw.case?.workItems.edges.find(
114
+ (edge) => edge.node.task.__typename === "CompleteWorkflowFormTask"
115
+ )?.node.id;
116
+
117
+ return rawId ? decodeId(rawId) : null;
118
+ }
117
119
 
118
120
  /**
119
121
  * All fields of all fieldsets of this document
120
122
  *
121
123
  * @property {Field[]} fields
122
- * @accessor
123
124
  */
124
- fields: computed("fieldsets.@each.fields", function () {
125
- return this.fieldsets.reduce(
126
- (fields, fieldset) => [...fields, ...fieldset.fields],
127
- []
128
- );
129
- }),
125
+ @cached
126
+ get fields() {
127
+ return this.fieldsets.flatMap((fieldset) => fieldset.fields);
128
+ }
130
129
 
131
130
  /**
132
131
  * The JEXL object for evaluating jexl expressions on this document
133
132
  *
134
133
  * @property {JEXL} jexl
135
- * @accessor
136
134
  */
137
- jexl: computed(function () {
135
+ @cached
136
+ get jexl() {
138
137
  const documentJexl = new jexl.Jexl();
139
138
 
140
139
  documentJexl.addTransform("answer", (slug, defaultValue) =>
@@ -174,31 +173,29 @@ export default Base.extend({
174
173
  documentJexl.addTransform("stringify", (input) => JSON.stringify(input));
175
174
 
176
175
  return documentJexl;
177
- }),
176
+ }
178
177
 
179
178
  /**
180
179
  * The JEXL context object for passing to the evaluation of jexl expessions
181
180
  *
182
181
  * @property {Object} jexlContext
183
- * @accessor
184
182
  */
185
- jexlContext: computed(
186
- "rootForm.{slug,meta}",
187
- "parentDocument.jexlContext",
188
- function () {
189
- if (this.parentDocument) return this.parentDocument.jexlContext;
190
-
191
- return {
183
+ get jexlContext() {
184
+ return (
185
+ this.parentDocument?.jexlContext ?? {
192
186
  // JEXL interprets null in an expression as variable instead of a
193
187
  // primitive. This resolves that issue.
194
188
  null: null,
195
189
  form: this.rootForm.slug,
196
190
  info: {
197
- root: { form: this.rootForm.slug, formMeta: this.rootForm.meta },
191
+ root: {
192
+ form: this.rootForm.slug,
193
+ formMeta: this.rootForm.raw.meta,
194
+ },
198
195
  },
199
- };
200
- }
201
- ),
196
+ }
197
+ );
198
+ }
202
199
 
203
200
  /**
204
201
  * Object representation of a document. The question slug as key and the
@@ -215,9 +212,9 @@ export default Base.extend({
215
212
  * answer.
216
213
  *
217
214
  * @property {Object} flatAnswerMap
218
- * @accessor
219
215
  */
220
- flatAnswerMap: computed("fields.@each.{question,value}", function () {
216
+ @cached
217
+ get flatAnswerMap() {
221
218
  return this.fields.reduce(
222
219
  (answerMap, field) => ({
223
220
  ...answerMap,
@@ -225,7 +222,7 @@ export default Base.extend({
225
222
  }),
226
223
  {}
227
224
  );
228
- }),
225
+ }
229
226
 
230
227
  /**
231
228
  * Find an answer for a given question slug
@@ -249,7 +246,7 @@ export default Base.extend({
249
246
  return defaultValue ?? field.question.isMultipleChoice ? [] : null;
250
247
  }
251
248
 
252
- if (field.question.__typename === "TableQuestion") {
249
+ if (field.question.isTable) {
253
250
  return field.value.map((doc) =>
254
251
  doc.fields
255
252
  .filter((field) => !field.hidden)
@@ -263,7 +260,7 @@ export default Base.extend({
263
260
  }
264
261
 
265
262
  return field.value;
266
- },
263
+ }
267
264
 
268
265
  /**
269
266
  * Find a field in the document by a given question slug
@@ -272,9 +269,8 @@ export default Base.extend({
272
269
  * @return {Field} The wanted field
273
270
  */
274
271
  findField(slug) {
275
- return [
276
- ...this.fields,
277
- ...(this.parentDocument ? this.parentDocument.fields : []),
278
- ].find((field) => field.question.slug === slug);
279
- },
280
- });
272
+ return [...this.fields, ...(this.parentDocument?.fields ?? [])].find(
273
+ (field) => field.question.slug === slug
274
+ );
275
+ }
276
+ }