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

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 (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
@@ -1,15 +1,14 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import { computed, defineProperty } from "@ember/object";
4
- import { equal, not, reads } from "@ember/object/computed";
3
+ import { associateDestroyableChild } from "@ember/destroyable";
5
4
  import { inject as service } from "@ember/service";
6
5
  import { camelize } from "@ember/string";
6
+ import { tracked } from "@glimmer/tracking";
7
7
  import { queryManager } from "ember-apollo-client";
8
- import { task } from "ember-concurrency";
8
+ import { restartableTask, lastValue, dropTask } from "ember-concurrency";
9
9
  import { validate } from "ember-validators";
10
- import cloneDeep from "lodash.clonedeep";
11
10
  import isEqual from "lodash.isequal";
12
- import { all, resolve } from "rsvp";
11
+ import { cached } from "tracked-toolbox";
13
12
 
14
13
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
15
14
  import saveDocumentDateAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-date-answer.graphql";
@@ -19,12 +18,9 @@ import saveDocumentIntegerAnswerMutation from "@projectcaluma/ember-form/gql/mut
19
18
  import saveDocumentListAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-list-answer.graphql";
20
19
  import saveDocumentStringAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-string-answer.graphql";
21
20
  import saveDocumentTableAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-table-answer.graphql";
22
- import getDocumentUsedDynamicOptionsQuery from "@projectcaluma/ember-form/gql/queries/get-document-used-dynamic-options.graphql";
21
+ import getDocumentUsedDynamicOptionsQuery from "@projectcaluma/ember-form/gql/queries/document-used-dynamic-options.graphql";
23
22
  import Base from "@projectcaluma/ember-form/lib/base";
24
- import {
25
- nestedDependencyParents,
26
- dependencies,
27
- } from "@projectcaluma/ember-form/lib/dependencies";
23
+ import dependencies from "@projectcaluma/ember-form/lib/dependencies";
28
24
 
29
25
  export const TYPE_MAP = {
30
26
  TextQuestion: "StringAnswer",
@@ -42,10 +38,20 @@ export const TYPE_MAP = {
42
38
  DateQuestion: "DateAnswer",
43
39
  };
44
40
 
45
- const fieldIsHidden = (field) => {
41
+ const MUTATION_MAP = {
42
+ FloatAnswer: saveDocumentFloatAnswerMutation,
43
+ IntegerAnswer: saveDocumentIntegerAnswerMutation,
44
+ StringAnswer: saveDocumentStringAnswerMutation,
45
+ ListAnswer: saveDocumentListAnswerMutation,
46
+ FileAnswer: saveDocumentFileAnswerMutation,
47
+ DateAnswer: saveDocumentDateAnswerMutation,
48
+ TableAnswer: saveDocumentTableAnswerMutation,
49
+ };
50
+
51
+ const fieldIsHiddenOrEmpty = (field) => {
46
52
  return (
47
53
  field.hidden ||
48
- (field.question.__typename !== "TableQuestion" &&
54
+ (!field.question.isTable &&
49
55
  (field.answer.value === null || field.answer.value === undefined))
50
56
  );
51
57
  };
@@ -55,61 +61,45 @@ const fieldIsHidden = (field) => {
55
61
  *
56
62
  * @class Field
57
63
  */
58
- export default Base.extend({
59
- saveDocumentFloatAnswerMutation,
60
- saveDocumentIntegerAnswerMutation,
61
- saveDocumentStringAnswerMutation,
62
- saveDocumentListAnswerMutation,
63
- saveDocumentFileAnswerMutation,
64
- saveDocumentDateAnswerMutation,
65
- saveDocumentTableAnswerMutation,
66
-
67
- intl: service(),
68
- calumaStore: service(),
69
- validator: service(),
70
-
71
- apollo: queryManager(),
72
-
73
- init(...args) {
74
- assert("A document must be passed", this.document);
75
-
76
- defineProperty(this, "pk", {
77
- writable: false,
78
- value: `${this.document.pk}:Question:${this.raw.question.slug}`,
79
- });
64
+ export default class Field extends Base {
65
+ @service intl;
66
+ @service validator;
80
67
 
81
- this._super(...args);
68
+ @queryManager apollo;
82
69
 
83
- this._createQuestion();
84
- this._createAnswer();
70
+ constructor({ fieldset, ...args }) {
71
+ assert("`fieldset` must be passed as an argument", fieldset);
85
72
 
86
- this.set("_errors", []);
87
- },
73
+ super({ fieldset, ...args });
88
74
 
89
- willDestroy(...args) {
90
- this._super(...args);
75
+ this.fieldset = fieldset;
91
76
 
92
- if (this.answer) {
93
- this.answer.destroy();
94
- }
95
- },
77
+ this.pushIntoStore();
78
+
79
+ this._createQuestion();
80
+ this._createAnswer();
81
+ }
96
82
 
97
83
  _createQuestion() {
84
+ const owner = getOwner(this);
85
+
98
86
  const question =
99
87
  this.calumaStore.find(`Question:${this.raw.question.slug}`) ||
100
- getOwner(this)
101
- .factoryFor("caluma-model:question")
102
- .create({ raw: this.raw.question });
88
+ new (owner.factoryFor("caluma-model:question").class)({
89
+ raw: this.raw.question,
90
+ owner,
91
+ });
103
92
 
104
93
  if (question.isDynamic) {
105
94
  question.loadDynamicOptions.perform();
106
95
  }
107
96
 
108
- this.set("question", question);
109
- },
97
+ this.question = question;
98
+ }
110
99
 
111
100
  _createAnswer() {
112
- const AnswerFactory = getOwner(this).factoryFor("caluma-model:answer");
101
+ const owner = getOwner(this);
102
+ const Answer = owner.factoryFor("caluma-model:answer").class;
113
103
  let answer;
114
104
 
115
105
  // no answer passed, create an empty one
@@ -121,182 +111,177 @@ export default Base.extend({
121
111
  return;
122
112
  }
123
113
 
124
- answer = AnswerFactory.create({
114
+ answer = new Answer({
125
115
  raw: {
126
116
  __typename: answerType,
127
117
  question: { slug: this.raw.question.slug },
128
118
  [camelize(answerType.replace(/Answer$/, "Value"))]: null,
129
119
  },
130
120
  field: this,
121
+ owner,
131
122
  });
132
123
  } else {
133
124
  answer =
134
125
  this.calumaStore.find(`Answer:${decodeId(this.raw.answer.id)}`) ||
135
- AnswerFactory.create({ raw: this.raw.answer, field: this });
126
+ new Answer({ raw: this.raw.answer, field: this, owner });
136
127
  }
137
128
 
138
- this.set("answer", answer);
139
- },
129
+ this.answer = associateDestroyableChild(this, answer);
130
+ }
140
131
 
141
132
  /**
142
133
  * The question to this field
143
134
  *
144
135
  * @property {Question} question
145
- * @accessor
146
136
  */
147
- question: null,
137
+ question = null;
148
138
 
149
139
  /**
150
140
  * The answer to this field. It is possible for this to be `null` if the
151
141
  * question is of the static question type.
152
142
  *
153
143
  * @property {Answer} answer
154
- * @accessor
155
144
  */
156
- answer: null,
145
+ answer = null;
146
+
147
+ /**
148
+ * The raw error objects which are later translated to readable messages.
149
+ *
150
+ * @property {Object[]} _errors
151
+ * @private
152
+ */
153
+ @tracked _errors = [];
154
+
155
+ /**
156
+ * The primary key of the field. Consists of the document and question primary
157
+ * keys.
158
+ *
159
+ * @property {String} pk
160
+ */
161
+ @cached
162
+ get pk() {
163
+ return `${this.document.pk}:Question:${this.raw.question.slug}`;
164
+ }
157
165
 
158
166
  /**
159
167
  * Whether the field is valid.
160
168
  *
161
169
  * @property {Boolean} isValid
162
- * @accessor
163
170
  */
164
- isValid: equal("errors.length", 0),
171
+ get isValid() {
172
+ return this.errors.length === 0;
173
+ }
165
174
 
166
175
  /**
167
176
  * Whether the field is invalid.
168
177
  *
169
178
  * @property {Boolean} isInvalid
170
- * @accessor
171
179
  */
172
- isInvalid: not("isValid"),
180
+ get isInvalid() {
181
+ return !this.isValid;
182
+ }
173
183
 
174
184
  /**
175
185
  * Whether the field is new (never saved to the backend service or empty)
176
186
  *
177
187
  * @property {Boolean} isNew
178
- * @accessor
179
188
  */
180
- isNew: computed("answer.isNew", function () {
189
+ get isNew() {
181
190
  return !this.answer || this.answer.isNew;
182
- }),
191
+ }
183
192
 
184
193
  /**
185
194
  * Only table values, this is used for certain computed property keys.
186
195
  *
187
196
  * @property {Document[]} tableValue
188
- * @accessor
189
197
  */
190
- tableValue: computed("value", "question.isTable", function () {
198
+ get tableValue() {
191
199
  return this.question.isTable ? this.value : [];
192
- }),
200
+ }
193
201
 
194
202
  /**
195
203
  * Whether the field has the defined default answer of the question as value.
196
204
  *
197
205
  * @property {Boolean} isDefault
198
- * @accessor
199
206
  */
200
- isDefault: computed(
201
- "value",
202
- "tableValue.@each.flatAnswerMap",
203
- "question.{isTable,defaultValue}",
204
- function () {
205
- if (!this.value || !this.question.defaultValue) return false;
207
+ get isDefault() {
208
+ if (!this.value || !this.question.defaultValue) return false;
206
209
 
207
- const value = this.question.isTable
208
- ? this.tableValue.map((doc) => doc.flatAnswerMap)
209
- : this.value;
210
+ const value = this.question.isTable
211
+ ? this.tableValue.map((doc) => doc.flatAnswerMap)
212
+ : this.value;
210
213
 
211
- return isEqual(value, this.question.defaultValue);
212
- }
213
- ),
214
+ return isEqual(value, this.question.defaultValue);
215
+ }
214
216
 
215
217
  /**
216
218
  * Whether the field is dirty. This will be true, if there is a value on the
217
219
  * answer which differs from the default value of the question.
218
220
  *
219
221
  * @property {Boolean} isDirty
220
- * @accessor
221
- */
222
- isDirty: computed(
223
- "isDefault",
224
- "isNew",
225
- "question.isCalculated",
226
- "validate.lastSuccessful",
227
- function () {
228
- if (this.question.isCalculated || this.isDefault) {
229
- return false;
230
- }
231
-
232
- return Boolean(this.validate.lastSuccessful) || !this.isNew;
222
+ */
223
+ get isDirty() {
224
+ if (this.question.isCalculated || this.isDefault) {
225
+ return false;
233
226
  }
234
- ),
227
+
228
+ return Boolean(this.validate.lastSuccessful) || !this.isNew;
229
+ }
235
230
 
236
231
  /**
237
232
  * The type of the question
238
233
  *
239
234
  * @property {String} questionType
240
- * @accessor
241
235
  */
242
- questionType: reads("question.__typename"),
236
+ get questionType() {
237
+ return this.question.raw.__typename;
238
+ }
243
239
 
244
240
  /**
245
241
  * The document this field belongs to
246
242
  *
247
243
  * @property {Document} document
248
- * @accessor
249
244
  */
250
- document: reads("fieldset.document"),
245
+ get document() {
246
+ return this.fieldset.document;
247
+ }
251
248
 
252
249
  /**
253
250
  * The value of the field
254
251
  *
255
252
  * @property {*} value
256
- * @accessor
257
- */
258
- value: computed(
259
- "question.isCalculated",
260
- "calculatedValue",
261
- "answer.value",
262
- function () {
263
- if (this.question.isCalculated) {
264
- return this.calculatedValue;
265
- }
266
-
267
- return this.answer?.value;
253
+ */
254
+ get value() {
255
+ if (this.question.isCalculated) {
256
+ return this.calculatedValue;
268
257
  }
269
- ),
258
+
259
+ return this.answer?.value;
260
+ }
270
261
 
271
262
  /**
272
- * The computed value of a caluclated question
263
+ * The computed value of a calculated float question
273
264
  *
274
265
  * @property {*} calculatedValue
275
- * @accessor
276
- */
277
- calculatedValue: computed(
278
- "document.jexl",
279
- "calculatedDependencies.@each.{hidden,value}",
280
- "jexlContext",
281
- "question.{isCalculated,calcExpression}",
282
- function () {
283
- if (
284
- !this.question.isCalculated ||
285
- !this.calculatedDependencies.every((field) => !field.hidden)
286
- ) {
287
- return null;
288
- }
266
+ */
267
+ @cached
268
+ get calculatedValue() {
269
+ if (
270
+ !this.question.isCalculated ||
271
+ !this.calculatedDependencies.every((field) => !field.hidden)
272
+ ) {
273
+ return null;
274
+ }
289
275
 
290
- try {
291
- return this.document.jexl.evalSync(
292
- this.question.calcExpression,
293
- this.jexlContext
294
- );
295
- } catch (error) {
296
- return null;
297
- }
276
+ try {
277
+ return this.document.jexl.evalSync(
278
+ this.question.raw.calcExpression,
279
+ this.jexlContext
280
+ );
281
+ } catch (error) {
282
+ return null;
298
283
  }
299
- ),
284
+ }
300
285
 
301
286
  /**
302
287
  * Fetch all formerly used dynamic options for this question. This will be
@@ -306,7 +291,8 @@ export default Base.extend({
306
291
  * @return {Object[]} Formerly used dynamic options
307
292
  * @private
308
293
  */
309
- _fetchUsedDynamicOptions: task(function* () {
294
+ @dropTask
295
+ *_fetchUsedDynamicOptions() {
310
296
  if (!this.question.isDynamic) return null;
311
297
 
312
298
  const edges = yield this.apollo.query(
@@ -325,16 +311,15 @@ export default Base.extend({
325
311
  slug,
326
312
  label,
327
313
  }));
328
- }),
314
+ }
329
315
 
330
316
  /**
331
317
  * The formerly used dynamic options for this question.
332
318
  *
333
319
  * @property {Object[]} usedDynamicOptions
334
- * @accessor
335
- *
336
320
  */
337
- usedDynamicOptions: reads("_fetchUsedDynamicOptions.lastSuccessful.value"),
321
+ @lastValue("_fetchUsedDynamicOptions")
322
+ usedDynamicOptions;
338
323
 
339
324
  /**
340
325
  * The available options for choice questions. This only works for the
@@ -348,79 +333,67 @@ export default Base.extend({
348
333
  * This will also return the disabled state of the option. An option can only
349
334
  * be disabled, if it is an old value used in a dynamic question.
350
335
  *
351
- * @property {Null|Object[]} options
352
- * @accessor
353
- */
354
- options: computed(
355
- "_fetchUsedDynamicOptions.lastSuccessful",
356
- "question.{options.[],isChoice,isDynamic,isMultipleChoice}",
357
- "usedDynamicOptions.[]",
358
- "value",
359
- function () {
360
- if (!this.question.isChoice && !this.question.isMultipleChoice) {
361
- return null;
362
- }
336
+ * @property {null|Object[]} options
337
+ */
338
+ @cached
339
+ get options() {
340
+ if (!this.question.isChoice && !this.question.isMultipleChoice) {
341
+ return null;
342
+ }
363
343
 
364
- const selected =
365
- (this.question.isMultipleChoice ? this.value : [this.value]) || [];
344
+ const selected =
345
+ (this.question.isMultipleChoice ? this.value : [this.value]) || [];
366
346
 
367
- const options = this.question.options.filter(
368
- (option) => !option.disabled || selected.includes(option.slug)
369
- );
347
+ const options = this.question.options.filter(
348
+ (option) => !option.disabled || selected.includes(option.slug)
349
+ );
370
350
 
371
- const hasUnknownValue = !selected.every((slug) =>
372
- options.find((option) => option.slug === slug)
373
- );
351
+ const hasUnknownValue = !selected.every((slug) =>
352
+ options.find((option) => option.slug === slug)
353
+ );
374
354
 
375
- if (this.question.isDynamic && hasUnknownValue) {
376
- if (!this._fetchUsedDynamicOptions.lastSuccessful) {
377
- // Fetch used dynamic options if not done yet
378
- this._fetchUsedDynamicOptions.perform();
379
- }
380
-
381
- return [
382
- ...options,
383
- ...(this.usedDynamicOptions || [])
384
- .filter((used) => {
385
- return (
386
- selected.includes(used.slug) &&
387
- !options.find((option) => option.slug === used.slug)
388
- );
389
- })
390
- .map((used) => ({ ...used, disabled: true })),
391
- ];
355
+ if (this.question.isDynamic && hasUnknownValue) {
356
+ if (!this._fetchUsedDynamicOptions.lastSuccessful) {
357
+ // Fetch used dynamic options if not done yet
358
+ this._fetchUsedDynamicOptions.perform();
392
359
  }
393
360
 
394
- return options;
361
+ return [
362
+ ...options,
363
+ ...(this.usedDynamicOptions || [])
364
+ .filter((used) => {
365
+ return (
366
+ selected.includes(used.slug) &&
367
+ !options.find((option) => option.slug === used.slug)
368
+ );
369
+ })
370
+ .map((used) => ({ ...used, disabled: true })),
371
+ ];
395
372
  }
396
- ),
373
+
374
+ return options;
375
+ }
397
376
 
398
377
  /**
399
378
  * The currently selected option. This property is only used for choice
400
379
  * questions. It can either return null if no value is selected yet, an
401
380
  * object for single choices or an array of objects for multiple choices.
402
381
  *
403
- * @property {Null|Object|Object[]} selected
404
- * @accessor
382
+ * @property {null|Object|Object[]} selected
405
383
  */
406
- selected: computed(
407
- "options.@each.slug",
408
- "question.{isChoice,isMultipleChoice}",
409
- "value",
410
- function () {
411
- if (!this.question.isChoice && !this.question.isMultipleChoice) {
412
- return null;
413
- }
384
+ get selected() {
385
+ if (!this.question.isChoice && !this.question.isMultipleChoice) {
386
+ return null;
387
+ }
414
388
 
415
- const selected = this.options.filter(({ slug }) =>
416
- this.question.isMultipleChoice
417
- ? (this.value || []).includes(slug)
418
- : this.value === slug
419
- );
389
+ const selected = this.options.filter(({ slug }) =>
390
+ this.question.isMultipleChoice
391
+ ? (this.value || []).includes(slug)
392
+ : this.value === slug
393
+ );
420
394
 
421
- return this.question.isMultipleChoice ? selected : selected[0];
422
- }
423
- ),
395
+ return this.question.isMultipleChoice ? selected : selected[0];
396
+ }
424
397
 
425
398
  /**
426
399
  * The field's JEXL context.
@@ -435,33 +408,25 @@ export default Base.extend({
435
408
  * - `info.root.formMeta`: The new property for the root form meta.
436
409
  *
437
410
  * @property {Object} jexlContext
438
- * @accessor
439
- */
440
- jexlContext: computed(
441
- "document.jexlContext",
442
- "fieldset.{form.slug,form.meta,field.fieldset.form.slug,field.fieldset.form.meta}",
443
- function () {
444
- const context = cloneDeep(this.document.jexlContext);
445
-
446
- const form = this.get("fieldset.form");
447
- const parent = this.get("fieldset.field.fieldset.form");
448
-
449
- return {
450
- ...context,
451
- info: {
452
- ...context.info,
453
- form: form.slug,
454
- formMeta: form.meta,
455
- parent: parent
456
- ? {
457
- form: parent.slug,
458
- formMeta: parent.meta,
459
- }
460
- : null,
461
- },
462
- };
463
- }
464
- ),
411
+ */
412
+ get jexlContext() {
413
+ const parent = this.fieldset.field?.fieldset.form;
414
+
415
+ return {
416
+ ...this.document.jexlContext,
417
+ info: {
418
+ ...this.document.jexlContext.info,
419
+ form: this.fieldset.form.slug,
420
+ formMeta: this.fieldset.form.raw.meta,
421
+ parent: parent
422
+ ? {
423
+ form: parent.slug,
424
+ formMeta: parent.raw.meta,
425
+ }
426
+ : null,
427
+ },
428
+ };
429
+ }
465
430
 
466
431
  /**
467
432
  * Fields that are referenced in the `calcExpression` JEXL expression
@@ -470,14 +435,8 @@ export default Base.extend({
470
435
  * expression needs to be re-evaluated.
471
436
  *
472
437
  * @property {Field[]} calculatedDependencies
473
- * @accessor
474
438
  */
475
- _calculatedNestedDependencyParents: nestedDependencyParents(
476
- "question.calcExpression"
477
- ),
478
- calculatedDependencies: dependencies("question.calcExpression", {
479
- nestedParentsPath: "_calculatedNestedDependencyParents",
480
- }),
439
+ @dependencies("question.raw.calcExpression") calculatedDependencies;
481
440
 
482
441
  /**
483
442
  * Fields that are referenced in the `isHidden` JEXL expression
@@ -486,12 +445,8 @@ export default Base.extend({
486
445
  * expression needs to be re-evaluated.
487
446
  *
488
447
  * @property {Field[]} hiddenDependencies
489
- * @accessor
490
448
  */
491
- _hiddenNestedDependencyParents: nestedDependencyParents("question.isHidden"),
492
- hiddenDependencies: dependencies("question.isHidden", {
493
- nestedParentsPath: "_hiddenNestedDependencyParents",
494
- }),
449
+ @dependencies("question.raw.isHidden") hiddenDependencies;
495
450
 
496
451
  /**
497
452
  * Fields that are referenced in the `isRequired` JEXL expression
@@ -500,14 +455,8 @@ export default Base.extend({
500
455
  * expression needs to be re-evaluated.
501
456
  *
502
457
  * @property {Field[]} optionalDependencies
503
- * @accessor
504
458
  */
505
- _optionalNestedDependencyParents: nestedDependencyParents(
506
- "question.isRequired"
507
- ),
508
- optionalDependencies: dependencies("question.isRequired", {
509
- nestedParentsPath: "_optionalNestedDependencyParents",
510
- }),
459
+ @dependencies("question.raw.isRequired") optionalDependencies;
511
460
 
512
461
  /**
513
462
  * The field's hidden state
@@ -515,99 +464,85 @@ export default Base.extend({
515
464
  * A question is hidden if:
516
465
  * - The form question field of the fieldset is hidden
517
466
  * - All depending field (used in the expression) are hidden
518
- * - The evaluated `question.isHidden` expression returns `true`
467
+ * - The evaluated `question.raw.isHidden` expression returns `true`
519
468
  *
520
469
  * @property {Boolean} hidden
521
470
  */
522
- hidden: computed(
523
- "document.jexl",
524
- "fieldset.field.hidden",
525
- "hiddenDependencies.@each.{hidden,value}",
526
- "jexlContext",
527
- "question.isHidden",
528
- "pk",
529
- function () {
530
- if (
531
- this.fieldset.field?.hidden ||
532
- (this.hiddenDependencies.length &&
533
- this.hiddenDependencies.every(fieldIsHidden))
534
- ) {
535
- return true;
536
- }
471
+ @cached
472
+ get hidden() {
473
+ if (
474
+ this.fieldset.field?.hidden ||
475
+ (this.hiddenDependencies.length &&
476
+ this.hiddenDependencies.every(fieldIsHiddenOrEmpty))
477
+ ) {
478
+ return true;
479
+ }
537
480
 
538
- try {
539
- return this.document.jexl.evalSync(
540
- this.question.isHidden,
541
- this.jexlContext
542
- );
543
- } catch (error) {
544
- throw new Error(
545
- `Error while evaluating \`isHidden\` expression on field \`${this.pk}\`: ${error.message}`
546
- );
547
- }
481
+ try {
482
+ return this.document.jexl.evalSync(
483
+ this.question.raw.isHidden,
484
+ this.jexlContext
485
+ );
486
+ } catch (error) {
487
+ throw new Error(
488
+ `Error while evaluating \`isHidden\` expression on field \`${this.pk}\`: ${error.message}`
489
+ );
548
490
  }
549
- ),
491
+ }
550
492
 
551
493
  /**
552
494
  * The field's optional state
553
495
  *
554
496
  * The field is optional if:
497
+ * - The question is of the type form or calculated float
555
498
  * - The form question field of the fieldset is hidden
556
499
  * - All depending fields (used in the expression) are hidden
557
- * - The evaluated `question.isRequired` expression returns `false`
500
+ * - The evaluated `question.raw.isRequired` expression returns `false`
558
501
  * - The question type is FormQuestion or CalculatedFloatQuestion
559
502
  *
560
503
  * @property {Boolean} optional
561
504
  */
562
- optional: computed(
563
- "document.jexl",
564
- "fieldset.field.hidden",
565
- "jexlContext",
566
- "optionalDependencies.@each.{hidden,value}",
567
- "question.{__typename,isRequired}",
568
- "pk",
569
- function () {
570
- if (
571
- this.fieldset.field?.hidden ||
572
- ["FormQuestion", "CalculatedFloatQuestion"].includes(
573
- this.question.__typename
574
- ) ||
575
- (this.optionalDependencies.length &&
576
- this.optionalDependencies.every(fieldIsHidden))
577
- ) {
578
- return true;
579
- }
505
+ @cached
506
+ get optional() {
507
+ if (
508
+ ["FormQuestion", "CalculatedFloatQuestion"].includes(this.questionType) ||
509
+ this.fieldset.field?.hidden ||
510
+ (this.optionalDependencies.length &&
511
+ this.optionalDependencies.every(fieldIsHiddenOrEmpty))
512
+ ) {
513
+ return true;
514
+ }
580
515
 
581
- try {
582
- return !this.document.jexl.evalSync(
583
- this.question.isRequired,
584
- this.jexlContext
585
- );
586
- } catch (error) {
587
- throw new Error(
588
- `Error while evaluating \`isRequired\` expression on field \`${this.pk}\`: ${error.message}`
589
- );
590
- }
516
+ try {
517
+ return !this.document.jexl.evalSync(
518
+ this.question.raw.isRequired,
519
+ this.jexlContext
520
+ );
521
+ } catch (error) {
522
+ throw new Error(
523
+ `Error while evaluating \`isRequired\` expression on field \`${this.pk}\`: ${error.message}`
524
+ );
591
525
  }
592
- ),
526
+ }
593
527
 
594
528
  /**
595
529
  * Task to save a field. This uses a different mutation for every answer
596
530
  * type.
597
531
  *
598
- * @method save.perform
532
+ * @method save
599
533
  * @return {Object} The response from the server
600
534
  */
601
- save: task(function* () {
535
+ @restartableTask
536
+ *save() {
602
537
  if (this.question.isCalculated) {
603
538
  return;
604
539
  }
605
540
 
606
- const type = this.get("answer.__typename");
541
+ const type = this.answer.raw.__typename;
607
542
 
608
543
  const response = yield this.apollo.mutate(
609
544
  {
610
- mutation: this.get(`saveDocument${type}Mutation`),
545
+ mutation: MUTATION_MAP[type],
611
546
  variables: {
612
547
  input: {
613
548
  question: this.question.slug,
@@ -621,49 +556,47 @@ export default Base.extend({
621
556
  `saveDocument${type}.answer`
622
557
  );
623
558
 
624
- if (this.isNew) {
625
- // if the answer was new we need to set a pk an push the answer to the
626
- // store
627
- this.answer.set("pk", `Answer:${decodeId(response.id)}`);
559
+ const wasNew = this.isNew;
628
560
 
629
- this.calumaStore.push(this.answer);
630
- }
631
-
632
- // update the existing answer
633
- this.answer.setProperties(response);
561
+ Object.entries(response).forEach(([key, value]) => {
562
+ this.answer.raw[key] = value;
563
+ });
634
564
 
635
- this.set("raw.answer", response);
636
- this.set("answer.raw", response);
565
+ if (wasNew) {
566
+ this.answer.pushIntoStore();
567
+ }
637
568
 
638
569
  return response;
639
- }).restartable(),
570
+ }
640
571
 
641
572
  /**
642
- * The error messages on this field.
573
+ * The translated error messages
643
574
  *
644
575
  * @property {String[]} errors
645
- * @accessor
646
576
  */
647
- errors: computed("_errors.[]", function () {
577
+ @cached
578
+ get errors() {
648
579
  return this._errors.map(({ type, context, value }) => {
649
580
  return this.intl.t(
650
581
  `caluma.form.validation.${type}`,
651
582
  Object.assign({}, context, { value })
652
583
  );
653
584
  });
654
- }),
585
+ }
655
586
 
656
587
  /**
657
588
  * Validate the field. Every field goes through the required validation and
658
589
  * the validation for the given question type. This mutates the `errors` on
659
590
  * the field.
660
591
  *
661
- * @method validate.perform
592
+ * @method validate
662
593
  */
663
- validate: task(function* () {
664
- const specificValidation = this.get(`_validate${this.question.__typename}`);
594
+ @restartableTask
595
+ *validate() {
596
+ const specificValidation = this[`_validate${this.questionType}`];
597
+
665
598
  assert(
666
- `Missing validation function for ${this.question.__typename}`,
599
+ `Missing validation function for ${this.questionType}`,
667
600
  specificValidation
668
601
  );
669
602
 
@@ -672,7 +605,7 @@ export default Base.extend({
672
605
  specificValidation,
673
606
  ];
674
607
 
675
- const errors = (yield all(
608
+ const errors = (yield Promise.all(
676
609
  validationFns.map(async (fn) => {
677
610
  const res = await fn.call(this);
678
611
 
@@ -682,121 +615,121 @@ export default Base.extend({
682
615
  .reduce((arr, e) => [...arr, ...e], []) // flatten the array
683
616
  .filter((e) => typeof e === "object");
684
617
 
685
- this.set("_errors", errors);
686
- }).restartable(),
618
+ this._errors = errors;
619
+ }
687
620
 
688
621
  /**
689
622
  * Method to validate if a question is required or not.
690
623
  *
691
624
  * @method _validateRequired
692
- * @return {RSVP.Promise} Returns an promise which resolves into an object if invalid or true if valid
693
- * @internal
625
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
626
+ * @private
694
627
  */
695
- async _validateRequired() {
628
+ _validateRequired() {
696
629
  return (
697
630
  this.optional ||
698
- validate("presence", this.get("answer.value"), { presence: true })
631
+ validate("presence", this.answer.value, { presence: true })
699
632
  );
700
- },
633
+ }
701
634
 
702
635
  /**
703
636
  * Method to validate a text question. This checks if the value longer than
704
637
  * predefined by the question.
705
638
  *
706
639
  * @method _validateTextQuestion
707
- * @return {Object|Boolean} Returns an object if invalid or true if valid
708
- * @internal
640
+ * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
641
+ * @private
709
642
  */
710
643
  async _validateTextQuestion() {
711
644
  return [
712
645
  ...(await this.validator.validate(
713
- this.get("answer.value"),
714
- this.get("question.meta.formatValidators") || []
646
+ this.answer.value,
647
+ this.question.raw.meta.formatValidators ?? []
715
648
  )),
716
- validate("length", this.get("answer.value"), {
717
- min: this.get("question.textMinLength") || 0,
718
- max: this.get("question.textMaxLength") || Number.POSITIVE_INFINITY,
649
+ validate("length", this.answer.value, {
650
+ min: this.question.raw.textMinLength || 0,
651
+ max: this.question.raw.textMaxLength || Number.POSITIVE_INFINITY,
719
652
  }),
720
653
  ];
721
- },
654
+ }
722
655
 
723
656
  /**
724
657
  * Method to validate a textarea question. This checks if the value longer
725
658
  * than predefined by the question.
726
659
  *
727
660
  * @method _validateTextareaQuestion
728
- * @return {Object|Boolean} Returns an object if invalid or true if valid
729
- * @internal
661
+ * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
662
+ * @private
730
663
  */
731
664
  async _validateTextareaQuestion() {
732
665
  return [
733
666
  ...(await this.validator.validate(
734
- this.get("answer.value"),
735
- this.get("question.meta.formatValidators") || []
667
+ this.answer.value,
668
+ this.question.raw.meta.formatValidators ?? []
736
669
  )),
737
- validate("length", this.get("answer.value"), {
738
- min: this.get("question.textareaMinLength") || 0,
739
- max: this.get("question.textareaMaxLength") || Number.POSITIVE_INFINITY,
670
+ validate("length", this.answer.value, {
671
+ min: this.question.raw.textareaMinLength || 0,
672
+ max: this.question.raw.textareaMaxLength || Number.POSITIVE_INFINITY,
740
673
  }),
741
674
  ];
742
- },
675
+ }
743
676
 
744
677
  /**
745
678
  * Method to validate an integer question. This checks if the value is bigger
746
679
  * or less than the options provided by the question.
747
680
  *
748
681
  * @method _validateIntegerQuestion
749
- * @return {Object|Boolean} Returns an object if invalid or true if valid
750
- * @internal
682
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
683
+ * @private
751
684
  */
752
685
  _validateIntegerQuestion() {
753
- return validate("number", this.get("answer.value"), {
686
+ return validate("number", this.answer.value, {
754
687
  integer: true,
755
- gte: this.get("question.integerMinValue") || Number.NEGATIVE_INFINITY,
756
- lte: this.get("question.integerMaxValue") || Number.POSITIVE_INFINITY,
688
+ gte: this.question.raw.integerMinValue || Number.NEGATIVE_INFINITY,
689
+ lte: this.question.raw.integerMaxValue || Number.POSITIVE_INFINITY,
757
690
  });
758
- },
691
+ }
759
692
 
760
693
  /**
761
694
  * Method to validate a float question. This checks if the value is bigger or
762
695
  * less than the options provided by the question.
763
696
  *
764
697
  * @method _validateFloatQuestion
765
- * @return {Object|Boolean} Returns an object if invalid or true if valid
766
- * @internal
698
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
699
+ * @private
767
700
  */
768
701
  _validateFloatQuestion() {
769
- return validate("number", this.get("answer.value"), {
770
- gte: this.get("question.floatMinValue") || Number.NEGATIVE_INFINITY,
771
- lte: this.get("question.floatMaxValue") || Number.POSITIVE_INFINITY,
702
+ return validate("number", this.answer.value, {
703
+ gte: this.question.raw.floatMinValue || Number.NEGATIVE_INFINITY,
704
+ lte: this.question.raw.floatMaxValue || Number.POSITIVE_INFINITY,
772
705
  });
773
- },
706
+ }
774
707
 
775
708
  /**
776
709
  * Method to validate a radio question. This checks if the value is included
777
710
  * in the provided options of the question.
778
711
  *
779
712
  * @method _validateChoiceQuestion
780
- * @return {Object|Boolean} Returns an object if invalid or true if valid
781
- * @internal
713
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
714
+ * @private
782
715
  */
783
716
  _validateChoiceQuestion() {
784
- return validate("inclusion", this.get("answer.value"), {
717
+ return validate("inclusion", this.answer.value, {
785
718
  allowBlank: true,
786
719
  in: (this.options || []).map(({ slug }) => slug),
787
720
  });
788
- },
721
+ }
789
722
 
790
723
  /**
791
724
  * Method to validate a checkbox question. This checks if the all of the
792
725
  * values are included in the provided options of the question.
793
726
  *
794
727
  * @method _validateMultipleChoiceQuestion
795
- * @return {Object[]|Boolean[]|Mixed[]} Returns per value an object if invalid or true if valid
796
- * @internal
728
+ * @return {Boolean|Object} Returns an object if invalid or true if valid
729
+ * @private
797
730
  */
798
731
  _validateMultipleChoiceQuestion() {
799
- const value = this.get("answer.value");
732
+ const value = this.answer.value;
800
733
  if (!value) {
801
734
  return true;
802
735
  }
@@ -805,34 +738,34 @@ export default Base.extend({
805
738
  in: (this.options || []).map(({ slug }) => slug),
806
739
  })
807
740
  );
808
- },
741
+ }
809
742
 
810
743
  /**
811
744
  * Method to validate a radio question. This checks if the value is included
812
745
  * in the provided options of the question.
813
746
  *
814
747
  * @method _validateChoiceQuestion
815
- * @return {Object|Boolean} Returns an object if invalid or true if valid
816
- * @internal
748
+ * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
749
+ * @private
817
750
  */
818
751
  async _validateDynamicChoiceQuestion() {
819
752
  await this.question.loadDynamicOptions.perform();
820
753
 
821
- return validate("inclusion", this.get("answer.value"), {
754
+ return validate("inclusion", this.answer.value, {
822
755
  in: (this.options || []).map(({ slug }) => slug),
823
756
  });
824
- },
757
+ }
825
758
 
826
759
  /**
827
760
  * Method to validate a checkbox question. This checks if the all of the
828
761
  * values are included in the provided options of the question.
829
762
  *
830
763
  * @method _validateMultipleChoiceQuestion
831
- * @return {Object[]|Boolean[]|Mixed[]} Returns per value an object if invalid or true if valid
832
- * @internal
764
+ * @return {Promise<Boolean[]|Object[]|Mixed[]>} A promise which resolves into an array of objects if invalid or true if valid
765
+ * @private
833
766
  */
834
767
  async _validateDynamicMultipleChoiceQuestion() {
835
- const value = this.get("answer.value");
768
+ const value = this.answer.value;
836
769
 
837
770
  if (!value) {
838
771
  return true;
@@ -845,45 +778,45 @@ export default Base.extend({
845
778
  in: (this.options || []).map(({ slug }) => slug),
846
779
  });
847
780
  });
848
- },
781
+ }
849
782
 
850
783
  /**
851
784
  * Dummy method for the validation of file uploads.
852
785
  *
853
786
  * @method _validateFileQuestion
854
- * @return {RSVP.Promise}
787
+ * @return {Boolean} Always returns true
855
788
  * @private
856
789
  */
857
790
  _validateFileQuestion() {
858
- return resolve(true);
859
- },
791
+ return true;
792
+ }
860
793
 
861
794
  /**
862
795
  * Method to validate a date question.
863
796
  *
864
797
  * @method _validateDateQuestion
865
798
  * @return {Object[]|Boolean[]|Mixed[]} Returns per value an object if invalid or true if valid
866
- * @internal
799
+ * @private
867
800
  */
868
801
  _validateDateQuestion() {
869
- return validate("date", this.get("answer.value"), {
802
+ return validate("date", this.answer.value, {
870
803
  allowBlank: true,
871
804
  });
872
- },
805
+ }
873
806
 
874
807
  /**
875
808
  * Dummy method for the validation of table fields
876
809
  *
877
810
  * @method _validateTableQuestion
878
- * @return {RSVP.Promise}
811
+ * @return {Promise<Boolean|Object>} A promise which resolves into an object if invalid or true if valid
879
812
  * @private
880
813
  */
881
814
  async _validateTableQuestion() {
882
815
  if (!this.value) return true;
883
816
 
884
- const rowValidations = await all(
817
+ const rowValidations = await Promise.all(
885
818
  this.value.map(async (row) => {
886
- const validFields = await all(
819
+ const validFields = await Promise.all(
887
820
  row.fields.map(async (field) => {
888
821
  await field.validate.perform();
889
822
 
@@ -902,49 +835,49 @@ export default Base.extend({
902
835
  value: null,
903
836
  }
904
837
  );
905
- },
838
+ }
906
839
 
907
840
  /**
908
841
  * Dummy method for the validation of static fields
909
842
  *
910
843
  * @method _validateStaticQuestion
911
- * @return {RSVP.Promise}
844
+ * @return {Boolean} Always returns true
912
845
  * @private
913
846
  */
914
847
  _validateStaticQuestion() {
915
- return resolve(true);
916
- },
848
+ return true;
849
+ }
917
850
 
918
851
  /**
919
852
  * Dummy method for the validation of form fields
920
853
  *
921
854
  * @method _validateFormQuestion
922
- * @return {RSVP.Promise}
855
+ * @return {Boolean} Always returns true
923
856
  * @private
924
857
  */
925
858
  _validateFormQuestion() {
926
- return resolve(true);
927
- },
859
+ return true;
860
+ }
928
861
 
929
862
  /**
930
863
  * Dummy method for the validation of calculated float fields
931
864
  *
932
865
  * @method _validateCalculatedFloatQuestion
933
- * @return {RSVP.Promise}
866
+ * @return {Boolean} Always returns true
934
867
  * @private
935
868
  */
936
869
  _validateCalculatedFloatQuestion() {
937
- return resolve(true);
938
- },
870
+ return true;
871
+ }
939
872
 
940
873
  /**
941
874
  * Dummy method for the validation of work item button fields
942
875
  *
943
876
  * @method _validateActionButtonQuestion
944
- * @return {RSVP.Promise}
877
+ * @return {Boolean} Always returns true
945
878
  * @private
946
879
  */
947
880
  _validateActionButtonQuestion() {
948
- return resolve(true);
949
- },
950
- });
881
+ return true;
882
+ }
883
+ }